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)) {
403 assertThrows(IllegalArgumentException.class, () -> {
404 webservice.putProcessAction(DEVICE_IDENTIFIER, ProcessAction.UNKNOWN);
410 public void testPutProcessActionThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
412 HttpFields responseHeaders = mock(HttpFields.class);
413 when(responseHeaders.containsKey(anyString())).thenReturn(false);
415 ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
416 when(response.getHeaders()).thenReturn(responseHeaders);
418 when(request.send()).thenReturn(response);
420 RequestFactory requestFactory = mock(RequestFactory.class);
421 when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"processAction\":1}"))
422 .thenReturn(request);
424 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
426 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
427 new DeviceStateDispatcher(), scheduler)) {
428 webservice.setAccessToken(ACCESS_TOKEN);
431 assertThrows(TooManyRequestsException.class, () -> {
432 webservice.putProcessAction(DEVICE_IDENTIFIER, ProcessAction.START);
438 public void testPutLightSendsRequestWithCorrectJsonContentWhenTurningTheLightOn() throws Exception {
440 ContentResponse response = createContentResponseMock(204, "");
441 when(request.send()).thenReturn(response);
443 RequestFactory requestFactory = mock(RequestFactory.class);
444 when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":1}")).thenReturn(request);
446 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
448 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
449 new DeviceStateDispatcher(), scheduler)) {
450 webservice.setAccessToken(ACCESS_TOKEN);
453 webservice.putLight(DEVICE_IDENTIFIER, true);
456 verify(request).send();
457 verifyNoMoreInteractions(request);
462 public void testPutLightSendsRequestWithCorrectJsonContentWhenTurningTheLightOff() throws Exception {
464 ContentResponse response = createContentResponseMock(204, "");
465 when(request.send()).thenReturn(response);
467 RequestFactory requestFactory = mock(RequestFactory.class);
468 when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":2}")).thenReturn(request);
470 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
472 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
473 new DeviceStateDispatcher(), scheduler)) {
474 webservice.setAccessToken(ACCESS_TOKEN);
477 webservice.putLight(DEVICE_IDENTIFIER, false);
480 verify(request).send();
481 verifyNoMoreInteractions(request);
486 public void testPutLightThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
488 HttpFields responseHeaders = mock(HttpFields.class);
489 when(responseHeaders.containsKey(anyString())).thenReturn(false);
491 ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
492 when(response.getHeaders()).thenReturn(responseHeaders);
494 when(request.send()).thenReturn(response);
496 RequestFactory requestFactory = mock(RequestFactory.class);
497 when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":2}")).thenReturn(request);
499 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
501 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
502 new DeviceStateDispatcher(), scheduler)) {
503 webservice.setAccessToken(ACCESS_TOKEN);
506 assertThrows(TooManyRequestsException.class, () -> {
507 webservice.putLight(DEVICE_IDENTIFIER, false);
513 public void testLogoutInvalidatesAccessTokenOnSuccess() throws Exception {
515 ContentResponse response = createContentResponseMock(204, "");
516 when(request.send()).thenReturn(response);
518 RequestFactory requestFactory = mock(RequestFactory.class);
519 when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
521 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
523 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
524 new DeviceStateDispatcher(), scheduler)) {
525 webservice.setAccessToken(ACCESS_TOKEN);
531 assertFalse(webservice.hasAccessToken());
532 verify(request).send();
533 verifyNoMoreInteractions(request);
538 public void testLogoutThrowsMieleWebserviceExceptionWhenMieleWebserviceTransientExceptionIsThrownInternally()
541 when(request.send()).thenThrow(TimeoutException.class);
543 RequestFactory requestFactory = mock(RequestFactory.class);
544 when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
546 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
548 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
549 new DeviceStateDispatcher(), scheduler)) {
550 webservice.setAccessToken(ACCESS_TOKEN);
553 assertThrows(MieleWebserviceException.class, () -> {
560 public void testLogoutInvalidatesAccessTokenWhenOperationFails() throws Exception {
562 when(request.send()).thenThrow(TimeoutException.class);
564 RequestFactory requestFactory = mock(RequestFactory.class);
565 when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
567 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
569 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
570 new DeviceStateDispatcher(), scheduler)) {
571 webservice.setAccessToken(ACCESS_TOKEN);
576 } catch (MieleWebserviceException e) {
580 assertFalse(webservice.hasAccessToken());
585 public void testRemoveDeviceStateListenerIsDelegatedToDeviceStateDispatcher() throws Exception {
587 RequestFactory requestFactory = mock(RequestFactory.class);
589 DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
591 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
593 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
594 dispatcher, scheduler)) {
595 DeviceStateListener listener = mock(DeviceStateListener.class);
596 webservice.addDeviceStateListener(listener);
599 webservice.removeDeviceStateListener(listener);
602 verify(dispatcher).addListener(listener);
603 verify(dispatcher).removeListener(listener);
604 verifyNoMoreInteractions(dispatcher);
609 public void testPutPowerStateSendsRequestWithCorrectJsonContentWhenSwitchingTheDeviceOn() throws Exception {
611 ContentResponse response = createContentResponseMock(204, "");
612 when(request.send()).thenReturn(response);
614 RequestFactory requestFactory = mock(RequestFactory.class);
615 when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOn\":true}")).thenReturn(request);
617 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
619 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
620 new DeviceStateDispatcher(), scheduler)) {
621 webservice.setAccessToken(ACCESS_TOKEN);
624 webservice.putPowerState(DEVICE_IDENTIFIER, true);
627 verify(request).send();
628 verifyNoMoreInteractions(request);
633 public void testPutPowerStateSendsRequestWithCorrectJsonContentWhenDeviceIsSwitchedOff() throws Exception {
635 ContentResponse response = createContentResponseMock(204, "");
636 when(request.send()).thenReturn(response);
638 RequestFactory requestFactory = mock(RequestFactory.class);
639 when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOff\":true}"))
640 .thenReturn(request);
642 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
644 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
645 new DeviceStateDispatcher(), scheduler)) {
646 webservice.setAccessToken(ACCESS_TOKEN);
649 webservice.putPowerState(DEVICE_IDENTIFIER, false);
652 verify(request).send();
653 verifyNoMoreInteractions(request);
658 public void testPutPowerStateThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
660 HttpFields responseHeaders = mock(HttpFields.class);
661 when(responseHeaders.containsKey(anyString())).thenReturn(false);
663 ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
664 when(response.getHeaders()).thenReturn(responseHeaders);
666 when(request.send()).thenReturn(response);
668 RequestFactory requestFactory = mock(RequestFactory.class);
669 when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOff\":true}"))
670 .thenReturn(request);
672 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
674 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
675 new DeviceStateDispatcher(), scheduler)) {
676 webservice.setAccessToken(ACCESS_TOKEN);
679 assertThrows(TooManyRequestsException.class, () -> {
680 webservice.putPowerState(DEVICE_IDENTIFIER, false);
686 public void testPutProgramResultsInARequestWithCorrectJson() throws Exception {
688 ContentResponse response = createContentResponseMock(204, "");
689 when(request.send()).thenReturn(response);
690 RequestFactory requestFactory = mock(RequestFactory.class);
691 when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"programId\":1}")).thenReturn(request);
693 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
695 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
696 new DeviceStateDispatcher(), scheduler)) {
697 webservice.setAccessToken(ACCESS_TOKEN);
700 webservice.putProgram(DEVICE_IDENTIFIER, 1);
703 verify(request).send();
704 verifyNoMoreInteractions(request);
709 public void testDispatchDeviceStateIsDelegatedToDeviceStateDispatcher() throws Exception {
711 RequestFactory requestFactory = mock(RequestFactory.class);
712 DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
713 ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
715 try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
716 dispatcher, scheduler)) {
718 webservice.dispatchDeviceState(DEVICE_IDENTIFIER);
721 verify(dispatcher).dispatchDeviceState(DEVICE_IDENTIFIER);
722 verifyNoMoreInteractions(dispatcher);
727 public void receivingSseActionsEventNotifiesConnectionAlive() throws Exception {
729 var requestFactory = mock(RequestFactory.class);
730 var dispatcher = mock(DeviceStateDispatcher.class);
731 var scheduler = mock(ScheduledExecutorService.class);
733 var connectionStatusListener = mock(ConnectionStatusListener.class);
735 try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
737 webservice.addConnectionStatusListener(connectionStatusListener);
739 var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS, "{}");
742 webservice.onServerSentEvent(actionsEvent);
745 verify(connectionStatusListener).onConnectionAlive();
750 public void receivingSseActionsEventWithNonJsonPayloadDoesNothing() throws Exception {
752 var requestFactory = mock(RequestFactory.class);
753 var dispatcher = mock(DeviceStateDispatcher.class);
754 var scheduler = mock(ScheduledExecutorService.class);
756 try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
758 webservice.setAccessToken(ACCESS_TOKEN);
760 var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
761 "{\"" + DEVICE_IDENTIFIER + "\": {}");
764 webservice.onServerSentEvent(actionsEvent);
767 verifyNoMoreInteractions(dispatcher);
772 public void receivingSseActionsEventFetchesActionsForADevice() throws Exception {
774 var requestFactory = mock(RequestFactory.class);
775 when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
777 var response = createContentResponseMock(200, "{}");
778 when(request.send()).thenReturn(response);
780 var dispatcher = mock(DeviceStateDispatcher.class);
781 var scheduler = mock(ScheduledExecutorService.class);
783 try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
785 webservice.setAccessToken(ACCESS_TOKEN);
787 var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
788 "{\"" + DEVICE_IDENTIFIER + "\": {}}");
791 webservice.onServerSentEvent(actionsEvent);
794 verify(dispatcher).dispatchActionStateUpdates(eq(DEVICE_IDENTIFIER), any());
795 verifyNoMoreInteractions(dispatcher);
800 public void receivingSseActionsEventFetchesActionsForMultipleDevices() throws Exception {
802 var otherRequest = mock(Request.class);
803 var otherDeviceIdentifier = "000124430017";
805 var requestFactory = mock(RequestFactory.class);
806 when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
807 when(requestFactory.createGetRequest(ENDPOINT_DEVICES + otherDeviceIdentifier + ENDPOINT_EXTENSION_ACTIONS,
808 ACCESS_TOKEN)).thenReturn(otherRequest);
810 var response = createContentResponseMock(200, "{}");
811 when(request.send()).thenReturn(response);
812 when(otherRequest.send()).thenReturn(response);
814 var dispatcher = mock(DeviceStateDispatcher.class);
815 var scheduler = mock(ScheduledExecutorService.class);
817 try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
819 webservice.setAccessToken(ACCESS_TOKEN);
821 var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
822 "{\"" + DEVICE_IDENTIFIER + "\": {}, \"" + otherDeviceIdentifier + "\": {}}");
825 webservice.onServerSentEvent(actionsEvent);
828 verify(dispatcher).dispatchActionStateUpdates(eq(DEVICE_IDENTIFIER), any());
829 verify(dispatcher).dispatchActionStateUpdates(eq(otherDeviceIdentifier), any());
830 verifyNoMoreInteractions(dispatcher);
835 public void whenFetchingActionsAfterReceivingSseActionsEventFailsForADeviceThenNothingHappensForThisDevice()
838 var otherRequest = mock(Request.class);
839 var otherDeviceIdentifier = "000124430017";
841 var requestFactory = mock(RequestFactory.class);
842 when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
843 when(requestFactory.createGetRequest(ENDPOINT_DEVICES + otherDeviceIdentifier + ENDPOINT_EXTENSION_ACTIONS,
844 ACCESS_TOKEN)).thenReturn(otherRequest);
846 var response = createContentResponseMock(200, "{}");
847 when(request.send()).thenReturn(response);
848 var otherResponse = createContentResponseMock(405, "{\"message\": \"HTTP 405 Method Not Allowed\"}");
849 when(otherRequest.send()).thenReturn(otherResponse);
851 var dispatcher = mock(DeviceStateDispatcher.class);
852 var scheduler = mock(ScheduledExecutorService.class);
854 try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
856 webservice.setAccessToken(ACCESS_TOKEN);
858 var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
859 "{\"" + DEVICE_IDENTIFIER + "\": {}, \"" + otherDeviceIdentifier + "\": {}}");
862 webservice.onServerSentEvent(actionsEvent);
865 verify(dispatcher).dispatchActionStateUpdates(eq(DEVICE_IDENTIFIER), any());
866 verifyNoMoreInteractions(dispatcher);
871 public void whenFetchingActionsAfterReceivingSseActionsEventFailsBecauseOfTooManyRequestsThenNothingHappens()
874 var requestFactory = mock(RequestFactory.class);
875 when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
877 var response = createContentResponseMock(429, "{\"message\": \"Too Many Requests\"}");
878 when(request.send()).thenReturn(response);
880 var headerFields = mock(HttpFields.class);
881 when(headerFields.containsKey(anyString())).thenReturn(false);
882 when(response.getHeaders()).thenReturn(headerFields);
884 var dispatcher = mock(DeviceStateDispatcher.class);
885 var scheduler = mock(ScheduledExecutorService.class);
887 try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
889 webservice.setAccessToken(ACCESS_TOKEN);
891 var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
892 "{\"" + DEVICE_IDENTIFIER + "\": {}}");
895 webservice.onServerSentEvent(actionsEvent);
898 verifyNoMoreInteractions(dispatcher);
903 public void whenFetchingActionsAfterReceivingSseActionsEventFailsBecauseOfAuthorizationFailedThenThisIsNotifiedToListeners()
906 var requestFactory = mock(RequestFactory.class);
907 when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
909 var response = createContentResponseMock(401, "{\"message\": \"Unauthorized\"}");
910 when(request.send()).thenReturn(response);
912 var dispatcher = mock(DeviceStateDispatcher.class);
913 var scheduler = mock(ScheduledExecutorService.class);
915 var connectionStatusListener = mock(ConnectionStatusListener.class);
917 try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
919 webservice.addConnectionStatusListener(connectionStatusListener);
920 webservice.setAccessToken(ACCESS_TOKEN);
922 var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
923 "{\"" + DEVICE_IDENTIFIER + "\": {}}");
926 webservice.onServerSentEvent(actionsEvent);
929 verifyNoMoreInteractions(dispatcher);
930 verify(connectionStatusListener).onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
935 * {@link RetryStrategy} for testing purposes. No exceptions will be catched.
937 * @author Roland Edelhoff - Initial contribution.
939 private static class UncatchedRetryStrategy implements RetryStrategy {
942 public <@Nullable T> T performRetryableOperation(Supplier<T> operation,
943 Consumer<Exception> onTransientException) {
944 return operation.get();
948 public void performRetryableOperation(Runnable operation, Consumer<Exception> onTransientException) {