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.webservice;
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.ReflectionUtil.getPrivate;
20 import java.util.concurrent.ExecutionException;
21 import java.util.concurrent.ScheduledExecutorService;
22 import java.util.concurrent.TimeoutException;
23 import java.util.function.Consumer;
24 import java.util.function.Supplier;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.eclipse.jetty.client.api.ContentResponse;
29 import org.eclipse.jetty.client.api.Request;
30 import org.eclipse.jetty.http.HttpFields;
31 import org.junit.jupiter.api.Test;
32 import org.openhab.binding.mielecloud.internal.MieleCloudBindingTestConstants;
33 import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
34 import org.openhab.binding.mielecloud.internal.util.MockUtil;
35 import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
36 import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
37 import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
38 import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceTransientException;
39 import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
40 import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
41 import org.openhab.binding.mielecloud.internal.webservice.request.RequestFactory;
42 import org.openhab.binding.mielecloud.internal.webservice.retry.AuthorizationFailedRetryStrategy;
43 import org.openhab.binding.mielecloud.internal.webservice.retry.NTimesRetryStrategy;
44 import org.openhab.binding.mielecloud.internal.webservice.retry.RetryStrategy;
45 import org.openhab.binding.mielecloud.internal.webservice.retry.RetryStrategyCombiner;
46 import org.openhab.binding.mielecloud.internal.webservice.sse.ServerSentEvent;
47 import org.openhab.core.io.net.http.HttpClientFactory;
50 * @author Björn Lange - Initial contribution
53 public class DefaultMieleWebserviceTest {
54 private static final String MESSAGE_INTERNAL_SERVER_ERROR = "{\"message\": \"Internal Server Error\"}";
55 private static final String MESSAGE_SERVICE_UNAVAILABLE = "{\"message\": \"unavailable\"}";
56 private static final String MESSAGE_INVALID_JSON = "{\"abc123: \"äfgh\"}";
58 private static final String DEVICE_IDENTIFIER = "000124430016";
60 private static final String SERVER_ADDRESS = "https://api.mcs3.miele.com";
61 private static final String ENDPOINT_DEVICES = SERVER_ADDRESS + "/v1/devices/";
62 private static final String ENDPOINT_EXTENSION_ACTIONS = "/actions";
63 private static final String ENDPOINT_ACTIONS = ENDPOINT_DEVICES + DEVICE_IDENTIFIER + ENDPOINT_EXTENSION_ACTIONS;
64 private static final String ENDPOINT_LOGOUT = SERVER_ADDRESS + "/thirdparty/logout";
66 private static final String ACCESS_TOKEN = "DE_0123456789abcdef0123456789abcdef";
68 private final RetryStrategy retryStrategy = new UncatchedRetryStrategy();
69 private final Request request = mock(Request.class);
72 public void testDefaultRetryStrategyIsCombinationOfOneTimeRetryStrategyAndAuthorizationFailedStrategy()
75 HttpClientFactory httpClientFactory = mock(HttpClientFactory.class);
76 when(httpClientFactory.createHttpClient(anyString())).thenReturn(MockUtil.mockHttpClient());
77 LanguageProvider languageProvider = mock(LanguageProvider.class);
78 OAuthTokenRefresher tokenRefresher = mock(OAuthTokenRefresher.class);
79 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
82 DefaultMieleWebservice webservice = new DefaultMieleWebservice(MieleWebserviceConfiguration.builder()
83 .withHttpClientFactory(httpClientFactory).withLanguageProvider(languageProvider)
84 .withTokenRefresher(tokenRefresher).withServiceHandle(MieleCloudBindingTestConstants.SERVICE_HANDLE)
85 .withScheduler(scheduler).build());
88 RetryStrategy retryStrategy = getPrivate(webservice, "retryStrategy");
89 assertTrue(retryStrategy instanceof RetryStrategyCombiner);
91 RetryStrategy first = getPrivate(retryStrategy, "first");
92 assertTrue(first instanceof NTimesRetryStrategy);
93 int numberOfRetries = getPrivate(first, "numberOfRetries");
94 assertEquals(1, numberOfRetries);
96 RetryStrategy second = getPrivate(retryStrategy, "second");
97 assertTrue(second instanceof AuthorizationFailedRetryStrategy);
98 OAuthTokenRefresher internalTokenRefresher = getPrivate(second, "tokenRefresher");
99 assertEquals(tokenRefresher, internalTokenRefresher);
102 private ContentResponse createContentResponseMock(int errorCode, String content) {
103 ContentResponse response = mock(ContentResponse.class);
104 when(response.getStatus()).thenReturn(errorCode);
105 when(response.getContentAsString()).thenReturn(content);
109 private void performFetchActions() throws Exception {
110 RequestFactory requestFactory = mock(RequestFactory.class);
111 when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
113 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
115 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
116 new DeviceStateDispatcher(), scheduler)) {
117 webservice.setAccessToken(ACCESS_TOKEN);
119 webservice.fetchActions(DEVICE_IDENTIFIER);
123 private void performFetchActionsExpectingFailure(ConnectionError expectedError) throws Exception {
125 performFetchActions();
126 } catch (MieleWebserviceException e) {
127 assertEquals(expectedError, e.getConnectionError());
129 } catch (MieleWebserviceTransientException e) {
130 assertEquals(expectedError, e.getConnectionError());
136 public void testTimeoutExceptionWhilePerformingFetchActionsRequest() throws Exception {
138 when(request.send()).thenThrow(TimeoutException.class);
141 assertThrows(MieleWebserviceTransientException.class, () -> {
142 performFetchActionsExpectingFailure(ConnectionError.TIMEOUT);
147 public void test500InternalServerErrorWhilePerformingFetchActionsRequest() throws Exception {
149 ContentResponse contentResponse = createContentResponseMock(500, MESSAGE_INTERNAL_SERVER_ERROR);
150 when(request.send()).thenReturn(contentResponse);
153 assertThrows(MieleWebserviceTransientException.class, () -> {
154 performFetchActionsExpectingFailure(ConnectionError.SERVER_ERROR);
159 public void test503ServiceUnavailableWhilePerformingFetchActionsRequest() throws Exception {
161 ContentResponse contentResponse = createContentResponseMock(503, MESSAGE_SERVICE_UNAVAILABLE);
162 when(request.send()).thenReturn(contentResponse);
165 assertThrows(MieleWebserviceTransientException.class, () -> {
166 performFetchActionsExpectingFailure(ConnectionError.SERVICE_UNAVAILABLE);
171 public void testInvalidJsonWhilePerformingFetchActionsRequest() throws Exception {
173 ContentResponse contentResponse = createContentResponseMock(200, MESSAGE_INVALID_JSON);
174 when(request.send()).thenReturn(contentResponse);
177 assertThrows(MieleWebserviceTransientException.class, () -> {
178 performFetchActionsExpectingFailure(ConnectionError.RESPONSE_MALFORMED);
183 public void testInterruptedExceptionWhilePerformingFetchActionsRequest() throws Exception {
185 when(request.send()).thenThrow(InterruptedException.class);
188 assertThrows(MieleWebserviceException.class, () -> {
189 performFetchActionsExpectingFailure(ConnectionError.REQUEST_INTERRUPTED);
194 public void testExecutionExceptionWhilePerformingFetchActionsRequest() throws Exception {
196 when(request.send()).thenThrow(ExecutionException.class);
199 assertThrows(MieleWebserviceException.class, () -> {
200 performFetchActionsExpectingFailure(ConnectionError.REQUEST_EXECUTION_FAILED);
205 public void test400BadRequestWhilePerformingFetchActionsRequest() throws Exception {
207 ContentResponse response = createContentResponseMock(400, "{\"message\": \"grant_type is invalid\"}");
208 when(request.send()).thenReturn(response);
211 assertThrows(MieleWebserviceException.class, () -> {
212 performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
217 public void test401UnauthorizedWhilePerformingFetchActionsRequest() throws Exception {
219 ContentResponse response = createContentResponseMock(401, "{\"message\": \"Unauthorized\"}");
220 when(request.send()).thenReturn(response);
223 assertThrows(AuthorizationFailedException.class, () -> {
224 performFetchActions();
229 public void test404NotFoundWhilePerformingFetchActionsRequest() throws Exception {
231 ContentResponse response = createContentResponseMock(404, "{\"message\": \"Not found\"}");
232 when(request.send()).thenReturn(response);
235 assertThrows(MieleWebserviceException.class, () -> {
236 performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
241 public void test405MethodNotAllowedWhilePerformingFetchActionsRequest() throws Exception {
243 ContentResponse response = createContentResponseMock(405, "{\"message\": \"HTTP 405 Method Not Allowed\"}");
244 when(request.send()).thenReturn(response);
247 assertThrows(MieleWebserviceException.class, () -> {
248 performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
253 public void test429TooManyRequestsWhilePerformingFetchActionsRequest() throws Exception {
255 HttpFields headerFields = mock(HttpFields.class);
256 when(headerFields.containsKey(anyString())).thenReturn(false);
258 ContentResponse response = createContentResponseMock(429, "{\"message\": \"Too Many Requests\"}");
259 when(response.getHeaders()).thenReturn(headerFields);
261 when(request.send()).thenReturn(response);
264 assertThrows(TooManyRequestsException.class, () -> {
265 performFetchActions();
270 public void test502BadGatewayWhilePerforminggFetchActionsRequest() throws Exception {
272 ContentResponse response = createContentResponseMock(502, "{\"message\": \"Bad Gateway\"}");
273 when(request.send()).thenReturn(response);
276 assertThrows(MieleWebserviceException.class, () -> {
277 performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
282 public void testMalformatedBodyWhilePerforminggFetchActionsRequest() throws Exception {
284 ContentResponse response = createContentResponseMock(502, "{\"message \"Bad Gateway\"}");
285 when(request.send()).thenReturn(response);
288 assertThrows(MieleWebserviceException.class, () -> {
289 performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
293 private void fillRequestMockWithDefaultContent() throws InterruptedException, TimeoutException, ExecutionException {
294 ContentResponse response = createContentResponseMock(200,
295 "{\"000124430016\":{\"ident\": {\"deviceName\": \"MyFancyHood\", \"deviceIdentLabel\": {\"fabNumber\": \"000124430016\"}}}}");
296 when(request.send()).thenReturn(response);
300 public void testAddDeviceStateListenerIsDelegatedToDeviceStateDispatcher() throws Exception {
302 RequestFactory requestFactory = mock(RequestFactory.class);
303 DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
304 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
306 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
307 dispatcher, scheduler)) {
308 DeviceStateListener listener = mock(DeviceStateListener.class);
311 webservice.addDeviceStateListener(listener);
314 verify(dispatcher).addListener(listener);
315 verifyNoMoreInteractions(dispatcher);
320 public void testFetchActionsDelegatesDeviceStateListenerDispatchingToDeviceStateDispatcher() throws Exception {
322 fillRequestMockWithDefaultContent();
324 RequestFactory requestFactory = mock(RequestFactory.class);
325 when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
327 DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
329 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
331 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy, dispatcher,
333 webservice.setAccessToken(ACCESS_TOKEN);
336 webservice.fetchActions(DEVICE_IDENTIFIER);
339 verify(dispatcher).dispatchActionStateUpdates(any(), any());
340 verifyNoMoreInteractions(dispatcher);
345 public void testFetchActionsThrowsMieleWebserviceTransientExceptionWhenRequestContentIsMalformatted()
348 ContentResponse response = createContentResponseMock(200, "{\"}");
349 when(request.send()).thenReturn(response);
351 RequestFactory requestFactory = mock(RequestFactory.class);
352 when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
354 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
356 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
357 new DeviceStateDispatcher(), scheduler)) {
358 webservice.setAccessToken(ACCESS_TOKEN);
361 assertThrows(MieleWebserviceTransientException.class, () -> {
362 webservice.fetchActions(DEVICE_IDENTIFIER);
368 public void testPutProcessActionSendsRequestWithCorrectJsonContent() throws Exception {
370 ContentResponse response = createContentResponseMock(204, "");
371 when(request.send()).thenReturn(response);
373 RequestFactory requestFactory = mock(RequestFactory.class);
374 when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"processAction\":1}"))
375 .thenReturn(request);
377 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
379 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
380 new DeviceStateDispatcher(), scheduler)) {
381 webservice.setAccessToken(ACCESS_TOKEN);
384 webservice.putProcessAction(DEVICE_IDENTIFIER, ProcessAction.START);
387 verify(request).send();
388 verifyNoMoreInteractions(request);
393 public void testPutProcessActionThrowsIllegalArgumentExceptionWhenProcessActionIsUnknown() throws Exception {
395 RequestFactory requestFactory = mock(RequestFactory.class);
397 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
399 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
400 new DeviceStateDispatcher(), scheduler)) {
402 assertThrows(IllegalArgumentException.class, () -> {
403 webservice.putProcessAction(DEVICE_IDENTIFIER, ProcessAction.UNKNOWN);
409 public void testPutProcessActionThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
411 HttpFields responseHeaders = mock(HttpFields.class);
412 when(responseHeaders.containsKey(anyString())).thenReturn(false);
414 ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
415 when(response.getHeaders()).thenReturn(responseHeaders);
417 when(request.send()).thenReturn(response);
419 RequestFactory requestFactory = mock(RequestFactory.class);
420 when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"processAction\":1}"))
421 .thenReturn(request);
423 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
425 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
426 new DeviceStateDispatcher(), scheduler)) {
427 webservice.setAccessToken(ACCESS_TOKEN);
430 assertThrows(TooManyRequestsException.class, () -> {
431 webservice.putProcessAction(DEVICE_IDENTIFIER, ProcessAction.START);
437 public void testPutLightSendsRequestWithCorrectJsonContentWhenTurningTheLightOn() throws Exception {
439 ContentResponse response = createContentResponseMock(204, "");
440 when(request.send()).thenReturn(response);
442 RequestFactory requestFactory = mock(RequestFactory.class);
443 when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":1}")).thenReturn(request);
445 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
447 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
448 new DeviceStateDispatcher(), scheduler)) {
449 webservice.setAccessToken(ACCESS_TOKEN);
452 webservice.putLight(DEVICE_IDENTIFIER, true);
455 verify(request).send();
456 verifyNoMoreInteractions(request);
461 public void testPutLightSendsRequestWithCorrectJsonContentWhenTurningTheLightOff() throws Exception {
463 ContentResponse response = createContentResponseMock(204, "");
464 when(request.send()).thenReturn(response);
466 RequestFactory requestFactory = mock(RequestFactory.class);
467 when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":2}")).thenReturn(request);
469 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
471 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
472 new DeviceStateDispatcher(), scheduler)) {
473 webservice.setAccessToken(ACCESS_TOKEN);
476 webservice.putLight(DEVICE_IDENTIFIER, false);
479 verify(request).send();
480 verifyNoMoreInteractions(request);
485 public void testPutLightThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
487 HttpFields responseHeaders = mock(HttpFields.class);
488 when(responseHeaders.containsKey(anyString())).thenReturn(false);
490 ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
491 when(response.getHeaders()).thenReturn(responseHeaders);
493 when(request.send()).thenReturn(response);
495 RequestFactory requestFactory = mock(RequestFactory.class);
496 when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":2}")).thenReturn(request);
498 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
500 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
501 new DeviceStateDispatcher(), scheduler)) {
502 webservice.setAccessToken(ACCESS_TOKEN);
505 assertThrows(TooManyRequestsException.class, () -> {
506 webservice.putLight(DEVICE_IDENTIFIER, false);
512 public void testLogoutInvalidatesAccessTokenOnSuccess() throws Exception {
514 ContentResponse response = createContentResponseMock(204, "");
515 when(request.send()).thenReturn(response);
517 RequestFactory requestFactory = mock(RequestFactory.class);
518 when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
520 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
522 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
523 new DeviceStateDispatcher(), scheduler)) {
524 webservice.setAccessToken(ACCESS_TOKEN);
530 assertFalse(webservice.hasAccessToken());
531 verify(request).send();
532 verifyNoMoreInteractions(request);
537 public void testLogoutThrowsMieleWebserviceExceptionWhenMieleWebserviceTransientExceptionIsThrownInternally()
540 when(request.send()).thenThrow(TimeoutException.class);
542 RequestFactory requestFactory = mock(RequestFactory.class);
543 when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
545 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
547 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
548 new DeviceStateDispatcher(), scheduler)) {
549 webservice.setAccessToken(ACCESS_TOKEN);
552 assertThrows(MieleWebserviceException.class, () -> {
559 public void testLogoutInvalidatesAccessTokenWhenOperationFails() throws Exception {
561 when(request.send()).thenThrow(TimeoutException.class);
563 RequestFactory requestFactory = mock(RequestFactory.class);
564 when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
566 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
568 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
569 new DeviceStateDispatcher(), scheduler)) {
570 webservice.setAccessToken(ACCESS_TOKEN);
575 } catch (MieleWebserviceException e) {
579 assertFalse(webservice.hasAccessToken());
584 public void testRemoveDeviceStateListenerIsDelegatedToDeviceStateDispatcher() throws Exception {
586 RequestFactory requestFactory = mock(RequestFactory.class);
588 DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
590 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
592 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
593 dispatcher, scheduler)) {
594 DeviceStateListener listener = mock(DeviceStateListener.class);
595 webservice.addDeviceStateListener(listener);
598 webservice.removeDeviceStateListener(listener);
601 verify(dispatcher).addListener(listener);
602 verify(dispatcher).removeListener(listener);
603 verifyNoMoreInteractions(dispatcher);
608 public void testPutPowerStateSendsRequestWithCorrectJsonContentWhenSwitchingTheDeviceOn() throws Exception {
610 ContentResponse response = createContentResponseMock(204, "");
611 when(request.send()).thenReturn(response);
613 RequestFactory requestFactory = mock(RequestFactory.class);
614 when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOn\":true}")).thenReturn(request);
616 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
618 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
619 new DeviceStateDispatcher(), scheduler)) {
620 webservice.setAccessToken(ACCESS_TOKEN);
623 webservice.putPowerState(DEVICE_IDENTIFIER, true);
626 verify(request).send();
627 verifyNoMoreInteractions(request);
632 public void testPutPowerStateSendsRequestWithCorrectJsonContentWhenDeviceIsSwitchedOff() throws Exception {
634 ContentResponse response = createContentResponseMock(204, "");
635 when(request.send()).thenReturn(response);
637 RequestFactory requestFactory = mock(RequestFactory.class);
638 when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOff\":true}"))
639 .thenReturn(request);
641 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
643 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
644 new DeviceStateDispatcher(), scheduler)) {
645 webservice.setAccessToken(ACCESS_TOKEN);
648 webservice.putPowerState(DEVICE_IDENTIFIER, false);
651 verify(request).send();
652 verifyNoMoreInteractions(request);
657 public void testPutPowerStateThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
659 HttpFields responseHeaders = mock(HttpFields.class);
660 when(responseHeaders.containsKey(anyString())).thenReturn(false);
662 ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
663 when(response.getHeaders()).thenReturn(responseHeaders);
665 when(request.send()).thenReturn(response);
667 RequestFactory requestFactory = mock(RequestFactory.class);
668 when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOff\":true}"))
669 .thenReturn(request);
671 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
673 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
674 new DeviceStateDispatcher(), scheduler)) {
675 webservice.setAccessToken(ACCESS_TOKEN);
678 assertThrows(TooManyRequestsException.class, () -> {
679 webservice.putPowerState(DEVICE_IDENTIFIER, false);
685 public void testPutProgramResultsInARequestWithCorrectJson() throws Exception {
687 ContentResponse response = createContentResponseMock(204, "");
688 when(request.send()).thenReturn(response);
689 RequestFactory requestFactory = mock(RequestFactory.class);
690 when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"programId\":1}")).thenReturn(request);
692 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
694 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
695 new DeviceStateDispatcher(), scheduler)) {
696 webservice.setAccessToken(ACCESS_TOKEN);
699 webservice.putProgram(DEVICE_IDENTIFIER, 1);
702 verify(request).send();
703 verifyNoMoreInteractions(request);
708 public void testDispatchDeviceStateIsDelegatedToDeviceStateDispatcher() throws Exception {
710 RequestFactory requestFactory = mock(RequestFactory.class);
711 DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
712 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
714 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
715 dispatcher, scheduler)) {
717 webservice.dispatchDeviceState(DEVICE_IDENTIFIER);
720 verify(dispatcher).dispatchDeviceState(DEVICE_IDENTIFIER);
721 verifyNoMoreInteractions(dispatcher);
726 public void receivingSseActionsEventNotifiesConnectionAlive() throws Exception {
728 var requestFactory = mock(RequestFactory.class);
729 var dispatcher = mock(DeviceStateDispatcher.class);
730 var scheduler = mock(ScheduledExecutorService.class);
732 var connectionStatusListener = mock(ConnectionStatusListener.class);
734 try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
736 webservice.addConnectionStatusListener(connectionStatusListener);
738 var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS, "{}");
741 webservice.onServerSentEvent(actionsEvent);
744 verify(connectionStatusListener).onConnectionAlive();
749 public void receivingSseActionsEventWithNonJsonPayloadDoesNothing() throws Exception {
751 var requestFactory = mock(RequestFactory.class);
752 var dispatcher = mock(DeviceStateDispatcher.class);
753 var scheduler = mock(ScheduledExecutorService.class);
755 try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
757 webservice.setAccessToken(ACCESS_TOKEN);
759 var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
760 "{\"" + DEVICE_IDENTIFIER + "\": {}");
763 webservice.onServerSentEvent(actionsEvent);
766 verifyNoMoreInteractions(dispatcher);
771 public void receivingSseActionsEventFetchesActionsForADevice() throws Exception {
773 var requestFactory = mock(RequestFactory.class);
774 when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
776 var response = createContentResponseMock(200, "{}");
777 when(request.send()).thenReturn(response);
779 var dispatcher = mock(DeviceStateDispatcher.class);
780 var scheduler = mock(ScheduledExecutorService.class);
782 try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
784 webservice.setAccessToken(ACCESS_TOKEN);
786 var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
787 "{\"" + DEVICE_IDENTIFIER + "\": {}}");
790 webservice.onServerSentEvent(actionsEvent);
793 verify(dispatcher).dispatchActionStateUpdates(eq(DEVICE_IDENTIFIER), any());
794 verifyNoMoreInteractions(dispatcher);
799 public void receivingSseActionsEventFetchesActionsForMultipleDevices() throws Exception {
801 var otherRequest = mock(Request.class);
802 var otherDeviceIdentifier = "000124430017";
804 var requestFactory = mock(RequestFactory.class);
805 when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
806 when(requestFactory.createGetRequest(ENDPOINT_DEVICES + otherDeviceIdentifier + ENDPOINT_EXTENSION_ACTIONS,
807 ACCESS_TOKEN)).thenReturn(otherRequest);
809 var response = createContentResponseMock(200, "{}");
810 when(request.send()).thenReturn(response);
811 when(otherRequest.send()).thenReturn(response);
813 var dispatcher = mock(DeviceStateDispatcher.class);
814 var scheduler = mock(ScheduledExecutorService.class);
816 try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
818 webservice.setAccessToken(ACCESS_TOKEN);
820 var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
821 "{\"" + DEVICE_IDENTIFIER + "\": {}, \"" + otherDeviceIdentifier + "\": {}}");
824 webservice.onServerSentEvent(actionsEvent);
827 verify(dispatcher).dispatchActionStateUpdates(eq(DEVICE_IDENTIFIER), any());
828 verify(dispatcher).dispatchActionStateUpdates(eq(otherDeviceIdentifier), any());
829 verifyNoMoreInteractions(dispatcher);
834 public void whenFetchingActionsAfterReceivingSseActionsEventFailsForADeviceThenNothingHappensForThisDevice()
837 var otherRequest = mock(Request.class);
838 var otherDeviceIdentifier = "000124430017";
840 var requestFactory = mock(RequestFactory.class);
841 when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
842 when(requestFactory.createGetRequest(ENDPOINT_DEVICES + otherDeviceIdentifier + ENDPOINT_EXTENSION_ACTIONS,
843 ACCESS_TOKEN)).thenReturn(otherRequest);
845 var response = createContentResponseMock(200, "{}");
846 when(request.send()).thenReturn(response);
847 var otherResponse = createContentResponseMock(405, "{\"message\": \"HTTP 405 Method Not Allowed\"}");
848 when(otherRequest.send()).thenReturn(otherResponse);
850 var dispatcher = mock(DeviceStateDispatcher.class);
851 var scheduler = mock(ScheduledExecutorService.class);
853 try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
855 webservice.setAccessToken(ACCESS_TOKEN);
857 var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
858 "{\"" + DEVICE_IDENTIFIER + "\": {}, \"" + otherDeviceIdentifier + "\": {}}");
861 webservice.onServerSentEvent(actionsEvent);
864 verify(dispatcher).dispatchActionStateUpdates(eq(DEVICE_IDENTIFIER), any());
865 verifyNoMoreInteractions(dispatcher);
870 public void whenFetchingActionsAfterReceivingSseActionsEventFailsBecauseOfTooManyRequestsThenNothingHappens()
873 var requestFactory = mock(RequestFactory.class);
874 when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
876 var response = createContentResponseMock(429, "{\"message\": \"Too Many Requests\"}");
877 when(request.send()).thenReturn(response);
879 var headerFields = mock(HttpFields.class);
880 when(headerFields.containsKey(anyString())).thenReturn(false);
881 when(response.getHeaders()).thenReturn(headerFields);
883 var dispatcher = mock(DeviceStateDispatcher.class);
884 var scheduler = mock(ScheduledExecutorService.class);
886 try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
888 webservice.setAccessToken(ACCESS_TOKEN);
890 var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
891 "{\"" + DEVICE_IDENTIFIER + "\": {}}");
894 webservice.onServerSentEvent(actionsEvent);
897 verifyNoMoreInteractions(dispatcher);
902 public void whenFetchingActionsAfterReceivingSseActionsEventFailsBecauseOfAuthorizationFailedThenThisIsNotifiedToListeners()
905 var requestFactory = mock(RequestFactory.class);
906 when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
908 var response = createContentResponseMock(401, "{\"message\": \"Unauthorized\"}");
909 when(request.send()).thenReturn(response);
911 var dispatcher = mock(DeviceStateDispatcher.class);
912 var scheduler = mock(ScheduledExecutorService.class);
914 var connectionStatusListener = mock(ConnectionStatusListener.class);
916 try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
918 webservice.addConnectionStatusListener(connectionStatusListener);
919 webservice.setAccessToken(ACCESS_TOKEN);
921 var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
922 "{\"" + DEVICE_IDENTIFIER + "\": {}}");
925 webservice.onServerSentEvent(actionsEvent);
928 verifyNoMoreInteractions(dispatcher);
929 verify(connectionStatusListener).onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
934 * {@link RetryStrategy} for testing purposes. No exceptions will be catched.
936 * @author Roland Edelhoff - Initial contribution.
938 private static class UncatchedRetryStrategy implements RetryStrategy {
941 public <@Nullable T> T performRetryableOperation(Supplier<T> operation,
942 Consumer<Exception> onTransientException) {
943 return operation.get();
947 public void performRetryableOperation(Runnable operation, Consumer<Exception> onTransientException) {