]> git.basschouten.com Git - openhab-addons.git/blob
da10dd4f9ae00213f8f4fc4eb1203faf50d7f976
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mielecloud.internal.webservice;
14
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;
19
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;
25
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;
48
49 /**
50  * @author Björn Lange - Initial contribution
51  */
52 @NonNullByDefault
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\"}";
57
58     private static final String DEVICE_IDENTIFIER = "000124430016";
59
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";
65
66     private static final String ACCESS_TOKEN = "DE_0123456789abcdef0123456789abcdef";
67
68     private final RetryStrategy retryStrategy = new UncatchedRetryStrategy();
69     private final Request request = mock(Request.class);
70
71     @Test
72     public void testDefaultRetryStrategyIsCombinationOfOneTimeRetryStrategyAndAuthorizationFailedStrategy()
73             throws Exception {
74         // given:
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);
80
81         // when:
82         DefaultMieleWebservice webservice = new DefaultMieleWebservice(MieleWebserviceConfiguration.builder()
83                 .withHttpClientFactory(httpClientFactory).withLanguageProvider(languageProvider)
84                 .withTokenRefresher(tokenRefresher).withServiceHandle(MieleCloudBindingTestConstants.SERVICE_HANDLE)
85                 .withScheduler(scheduler).build());
86
87         // then:
88         RetryStrategy retryStrategy = getPrivate(webservice, "retryStrategy");
89         assertTrue(retryStrategy instanceof RetryStrategyCombiner);
90
91         RetryStrategy first = getPrivate(retryStrategy, "first");
92         assertTrue(first instanceof NTimesRetryStrategy);
93         int numberOfRetries = getPrivate(first, "numberOfRetries");
94         assertEquals(1, numberOfRetries);
95
96         RetryStrategy second = getPrivate(retryStrategy, "second");
97         assertTrue(second instanceof AuthorizationFailedRetryStrategy);
98         OAuthTokenRefresher internalTokenRefresher = getPrivate(second, "tokenRefresher");
99         assertEquals(tokenRefresher, internalTokenRefresher);
100     }
101
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);
106         return response;
107     }
108
109     private void performFetchActions() throws Exception {
110         RequestFactory requestFactory = mock(RequestFactory.class);
111         when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
112
113         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
114
115         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
116                 new DeviceStateDispatcher(), scheduler)) {
117             webservice.setAccessToken(ACCESS_TOKEN);
118
119             webservice.fetchActions(DEVICE_IDENTIFIER);
120         }
121     }
122
123     private void performFetchActionsExpectingFailure(ConnectionError expectedError) throws Exception {
124         try {
125             performFetchActions();
126         } catch (MieleWebserviceException e) {
127             assertEquals(expectedError, e.getConnectionError());
128             throw e;
129         } catch (MieleWebserviceTransientException e) {
130             assertEquals(expectedError, e.getConnectionError());
131             throw e;
132         }
133     }
134
135     @Test
136     public void testTimeoutExceptionWhilePerformingFetchActionsRequest() throws Exception {
137         // given:
138         when(request.send()).thenThrow(TimeoutException.class);
139
140         // when:
141         assertThrows(MieleWebserviceTransientException.class, () -> {
142             performFetchActionsExpectingFailure(ConnectionError.TIMEOUT);
143         });
144     }
145
146     @Test
147     public void test500InternalServerErrorWhilePerformingFetchActionsRequest() throws Exception {
148         // given:
149         ContentResponse contentResponse = createContentResponseMock(500, MESSAGE_INTERNAL_SERVER_ERROR);
150         when(request.send()).thenReturn(contentResponse);
151
152         // when:
153         assertThrows(MieleWebserviceTransientException.class, () -> {
154             performFetchActionsExpectingFailure(ConnectionError.SERVER_ERROR);
155         });
156     }
157
158     @Test
159     public void test503ServiceUnavailableWhilePerformingFetchActionsRequest() throws Exception {
160         // given:
161         ContentResponse contentResponse = createContentResponseMock(503, MESSAGE_SERVICE_UNAVAILABLE);
162         when(request.send()).thenReturn(contentResponse);
163
164         // when:
165         assertThrows(MieleWebserviceTransientException.class, () -> {
166             performFetchActionsExpectingFailure(ConnectionError.SERVICE_UNAVAILABLE);
167         });
168     }
169
170     @Test
171     public void testInvalidJsonWhilePerformingFetchActionsRequest() throws Exception {
172         // given:
173         ContentResponse contentResponse = createContentResponseMock(200, MESSAGE_INVALID_JSON);
174         when(request.send()).thenReturn(contentResponse);
175
176         // when:
177         assertThrows(MieleWebserviceTransientException.class, () -> {
178             performFetchActionsExpectingFailure(ConnectionError.RESPONSE_MALFORMED);
179         });
180     }
181
182     @Test
183     public void testInterruptedExceptionWhilePerformingFetchActionsRequest() throws Exception {
184         // given:
185         when(request.send()).thenThrow(InterruptedException.class);
186
187         // when:
188         assertThrows(MieleWebserviceException.class, () -> {
189             performFetchActionsExpectingFailure(ConnectionError.REQUEST_INTERRUPTED);
190         });
191     }
192
193     @Test
194     public void testExecutionExceptionWhilePerformingFetchActionsRequest() throws Exception {
195         // given:
196         when(request.send()).thenThrow(ExecutionException.class);
197
198         // when:
199         assertThrows(MieleWebserviceException.class, () -> {
200             performFetchActionsExpectingFailure(ConnectionError.REQUEST_EXECUTION_FAILED);
201         });
202     }
203
204     @Test
205     public void test400BadRequestWhilePerformingFetchActionsRequest() throws Exception {
206         // given:
207         ContentResponse response = createContentResponseMock(400, "{\"message\": \"grant_type is invalid\"}");
208         when(request.send()).thenReturn(response);
209
210         // when:
211         assertThrows(MieleWebserviceException.class, () -> {
212             performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
213         });
214     }
215
216     @Test
217     public void test401UnauthorizedWhilePerformingFetchActionsRequest() throws Exception {
218         // given:
219         ContentResponse response = createContentResponseMock(401, "{\"message\": \"Unauthorized\"}");
220         when(request.send()).thenReturn(response);
221
222         // when:
223         assertThrows(AuthorizationFailedException.class, () -> {
224             performFetchActions();
225         });
226     }
227
228     @Test
229     public void test404NotFoundWhilePerformingFetchActionsRequest() throws Exception {
230         // given:
231         ContentResponse response = createContentResponseMock(404, "{\"message\": \"Not found\"}");
232         when(request.send()).thenReturn(response);
233
234         // when:
235         assertThrows(MieleWebserviceException.class, () -> {
236             performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
237         });
238     }
239
240     @Test
241     public void test405MethodNotAllowedWhilePerformingFetchActionsRequest() throws Exception {
242         // given:
243         ContentResponse response = createContentResponseMock(405, "{\"message\": \"HTTP 405 Method Not Allowed\"}");
244         when(request.send()).thenReturn(response);
245
246         // when:
247         assertThrows(MieleWebserviceException.class, () -> {
248             performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
249         });
250     }
251
252     @Test
253     public void test429TooManyRequestsWhilePerformingFetchActionsRequest() throws Exception {
254         // given:
255         HttpFields headerFields = mock(HttpFields.class);
256         when(headerFields.containsKey(anyString())).thenReturn(false);
257
258         ContentResponse response = createContentResponseMock(429, "{\"message\": \"Too Many Requests\"}");
259         when(response.getHeaders()).thenReturn(headerFields);
260
261         when(request.send()).thenReturn(response);
262
263         // when:
264         assertThrows(TooManyRequestsException.class, () -> {
265             performFetchActions();
266         });
267     }
268
269     @Test
270     public void test502BadGatewayWhilePerforminggFetchActionsRequest() throws Exception {
271         // given:
272         ContentResponse response = createContentResponseMock(502, "{\"message\": \"Bad Gateway\"}");
273         when(request.send()).thenReturn(response);
274
275         // when:
276         assertThrows(MieleWebserviceException.class, () -> {
277             performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
278         });
279     }
280
281     @Test
282     public void testMalformatedBodyWhilePerforminggFetchActionsRequest() throws Exception {
283         // given:
284         ContentResponse response = createContentResponseMock(502, "{\"message \"Bad Gateway\"}");
285         when(request.send()).thenReturn(response);
286
287         // when:
288         assertThrows(MieleWebserviceException.class, () -> {
289             performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
290         });
291     }
292
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);
297     }
298
299     @Test
300     public void testAddDeviceStateListenerIsDelegatedToDeviceStateDispatcher() throws Exception {
301         // given:
302         RequestFactory requestFactory = mock(RequestFactory.class);
303         DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
304         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
305
306         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
307                 dispatcher, scheduler)) {
308             DeviceStateListener listener = mock(DeviceStateListener.class);
309
310             // when:
311             webservice.addDeviceStateListener(listener);
312
313             // then:
314             verify(dispatcher).addListener(listener);
315             verifyNoMoreInteractions(dispatcher);
316         }
317     }
318
319     @Test
320     public void testFetchActionsDelegatesDeviceStateListenerDispatchingToDeviceStateDispatcher() throws Exception {
321         // given:
322         fillRequestMockWithDefaultContent();
323
324         RequestFactory requestFactory = mock(RequestFactory.class);
325         when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
326
327         DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
328
329         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
330
331         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy, dispatcher,
332                 scheduler)) {
333             webservice.setAccessToken(ACCESS_TOKEN);
334
335             // when:
336             webservice.fetchActions(DEVICE_IDENTIFIER);
337
338             // then:
339             verify(dispatcher).dispatchActionStateUpdates(any(), any());
340             verifyNoMoreInteractions(dispatcher);
341         }
342     }
343
344     @Test
345     public void testFetchActionsThrowsMieleWebserviceTransientExceptionWhenRequestContentIsMalformatted()
346             throws Exception {
347         // given:
348         ContentResponse response = createContentResponseMock(200, "{\"}");
349         when(request.send()).thenReturn(response);
350
351         RequestFactory requestFactory = mock(RequestFactory.class);
352         when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
353
354         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
355
356         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
357                 new DeviceStateDispatcher(), scheduler)) {
358             webservice.setAccessToken(ACCESS_TOKEN);
359
360             // when:
361             assertThrows(MieleWebserviceTransientException.class, () -> {
362                 webservice.fetchActions(DEVICE_IDENTIFIER);
363             });
364         }
365     }
366
367     @Test
368     public void testPutProcessActionSendsRequestWithCorrectJsonContent() throws Exception {
369         // given:
370         ContentResponse response = createContentResponseMock(204, "");
371         when(request.send()).thenReturn(response);
372
373         RequestFactory requestFactory = mock(RequestFactory.class);
374         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"processAction\":1}"))
375                 .thenReturn(request);
376
377         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
378
379         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
380                 new DeviceStateDispatcher(), scheduler)) {
381             webservice.setAccessToken(ACCESS_TOKEN);
382
383             // when:
384             webservice.putProcessAction(DEVICE_IDENTIFIER, ProcessAction.START);
385
386             // then:
387             verify(request).send();
388             verifyNoMoreInteractions(request);
389         }
390     }
391
392     @Test
393     public void testPutProcessActionThrowsIllegalArgumentExceptionWhenProcessActionIsUnknown() throws Exception {
394         // given:
395         RequestFactory requestFactory = mock(RequestFactory.class);
396
397         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
398
399         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
400                 new DeviceStateDispatcher(), scheduler)) {
401
402             // when:
403             assertThrows(IllegalArgumentException.class, () -> {
404                 webservice.putProcessAction(DEVICE_IDENTIFIER, ProcessAction.UNKNOWN);
405             });
406         }
407     }
408
409     @Test
410     public void testPutProcessActionThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
411         // given:
412         HttpFields responseHeaders = mock(HttpFields.class);
413         when(responseHeaders.containsKey(anyString())).thenReturn(false);
414
415         ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
416         when(response.getHeaders()).thenReturn(responseHeaders);
417
418         when(request.send()).thenReturn(response);
419
420         RequestFactory requestFactory = mock(RequestFactory.class);
421         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"processAction\":1}"))
422                 .thenReturn(request);
423
424         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
425
426         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
427                 new DeviceStateDispatcher(), scheduler)) {
428             webservice.setAccessToken(ACCESS_TOKEN);
429
430             // when:
431             assertThrows(TooManyRequestsException.class, () -> {
432                 webservice.putProcessAction(DEVICE_IDENTIFIER, ProcessAction.START);
433             });
434         }
435     }
436
437     @Test
438     public void testPutLightSendsRequestWithCorrectJsonContentWhenTurningTheLightOn() throws Exception {
439         // given:
440         ContentResponse response = createContentResponseMock(204, "");
441         when(request.send()).thenReturn(response);
442
443         RequestFactory requestFactory = mock(RequestFactory.class);
444         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":1}")).thenReturn(request);
445
446         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
447
448         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
449                 new DeviceStateDispatcher(), scheduler)) {
450             webservice.setAccessToken(ACCESS_TOKEN);
451
452             // when:
453             webservice.putLight(DEVICE_IDENTIFIER, true);
454
455             // then:
456             verify(request).send();
457             verifyNoMoreInteractions(request);
458         }
459     }
460
461     @Test
462     public void testPutLightSendsRequestWithCorrectJsonContentWhenTurningTheLightOff() throws Exception {
463         // given:
464         ContentResponse response = createContentResponseMock(204, "");
465         when(request.send()).thenReturn(response);
466
467         RequestFactory requestFactory = mock(RequestFactory.class);
468         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":2}")).thenReturn(request);
469
470         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
471
472         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
473                 new DeviceStateDispatcher(), scheduler)) {
474             webservice.setAccessToken(ACCESS_TOKEN);
475
476             // when:
477             webservice.putLight(DEVICE_IDENTIFIER, false);
478
479             // then:
480             verify(request).send();
481             verifyNoMoreInteractions(request);
482         }
483     }
484
485     @Test
486     public void testPutLightThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
487         // given:
488         HttpFields responseHeaders = mock(HttpFields.class);
489         when(responseHeaders.containsKey(anyString())).thenReturn(false);
490
491         ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
492         when(response.getHeaders()).thenReturn(responseHeaders);
493
494         when(request.send()).thenReturn(response);
495
496         RequestFactory requestFactory = mock(RequestFactory.class);
497         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":2}")).thenReturn(request);
498
499         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
500
501         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
502                 new DeviceStateDispatcher(), scheduler)) {
503             webservice.setAccessToken(ACCESS_TOKEN);
504
505             // when:
506             assertThrows(TooManyRequestsException.class, () -> {
507                 webservice.putLight(DEVICE_IDENTIFIER, false);
508             });
509         }
510     }
511
512     @Test
513     public void testLogoutInvalidatesAccessTokenOnSuccess() throws Exception {
514         // given:
515         ContentResponse response = createContentResponseMock(204, "");
516         when(request.send()).thenReturn(response);
517
518         RequestFactory requestFactory = mock(RequestFactory.class);
519         when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
520
521         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
522
523         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
524                 new DeviceStateDispatcher(), scheduler)) {
525             webservice.setAccessToken(ACCESS_TOKEN);
526
527             // when:
528             webservice.logout();
529
530             // then:
531             assertFalse(webservice.hasAccessToken());
532             verify(request).send();
533             verifyNoMoreInteractions(request);
534         }
535     }
536
537     @Test
538     public void testLogoutThrowsMieleWebserviceExceptionWhenMieleWebserviceTransientExceptionIsThrownInternally()
539             throws Exception {
540         // given:
541         when(request.send()).thenThrow(TimeoutException.class);
542
543         RequestFactory requestFactory = mock(RequestFactory.class);
544         when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
545
546         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
547
548         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
549                 new DeviceStateDispatcher(), scheduler)) {
550             webservice.setAccessToken(ACCESS_TOKEN);
551
552             // when:
553             assertThrows(MieleWebserviceException.class, () -> {
554                 webservice.logout();
555             });
556         }
557     }
558
559     @Test
560     public void testLogoutInvalidatesAccessTokenWhenOperationFails() throws Exception {
561         // given:
562         when(request.send()).thenThrow(TimeoutException.class);
563
564         RequestFactory requestFactory = mock(RequestFactory.class);
565         when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
566
567         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
568
569         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
570                 new DeviceStateDispatcher(), scheduler)) {
571             webservice.setAccessToken(ACCESS_TOKEN);
572
573             // when:
574             try {
575                 webservice.logout();
576             } catch (MieleWebserviceException e) {
577             }
578
579             // then:
580             assertFalse(webservice.hasAccessToken());
581         }
582     }
583
584     @Test
585     public void testRemoveDeviceStateListenerIsDelegatedToDeviceStateDispatcher() throws Exception {
586         // given:
587         RequestFactory requestFactory = mock(RequestFactory.class);
588
589         DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
590
591         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
592
593         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
594                 dispatcher, scheduler)) {
595             DeviceStateListener listener = mock(DeviceStateListener.class);
596             webservice.addDeviceStateListener(listener);
597
598             // when:
599             webservice.removeDeviceStateListener(listener);
600
601             // then:
602             verify(dispatcher).addListener(listener);
603             verify(dispatcher).removeListener(listener);
604             verifyNoMoreInteractions(dispatcher);
605         }
606     }
607
608     @Test
609     public void testPutPowerStateSendsRequestWithCorrectJsonContentWhenSwitchingTheDeviceOn() throws Exception {
610         // given:
611         ContentResponse response = createContentResponseMock(204, "");
612         when(request.send()).thenReturn(response);
613
614         RequestFactory requestFactory = mock(RequestFactory.class);
615         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOn\":true}")).thenReturn(request);
616
617         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
618
619         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
620                 new DeviceStateDispatcher(), scheduler)) {
621             webservice.setAccessToken(ACCESS_TOKEN);
622
623             // when:
624             webservice.putPowerState(DEVICE_IDENTIFIER, true);
625
626             // then:
627             verify(request).send();
628             verifyNoMoreInteractions(request);
629         }
630     }
631
632     @Test
633     public void testPutPowerStateSendsRequestWithCorrectJsonContentWhenDeviceIsSwitchedOff() throws Exception {
634         // given:
635         ContentResponse response = createContentResponseMock(204, "");
636         when(request.send()).thenReturn(response);
637
638         RequestFactory requestFactory = mock(RequestFactory.class);
639         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOff\":true}"))
640                 .thenReturn(request);
641
642         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
643
644         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
645                 new DeviceStateDispatcher(), scheduler)) {
646             webservice.setAccessToken(ACCESS_TOKEN);
647
648             // when:
649             webservice.putPowerState(DEVICE_IDENTIFIER, false);
650
651             // then:
652             verify(request).send();
653             verifyNoMoreInteractions(request);
654         }
655     }
656
657     @Test
658     public void testPutPowerStateThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
659         // given:
660         HttpFields responseHeaders = mock(HttpFields.class);
661         when(responseHeaders.containsKey(anyString())).thenReturn(false);
662
663         ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
664         when(response.getHeaders()).thenReturn(responseHeaders);
665
666         when(request.send()).thenReturn(response);
667
668         RequestFactory requestFactory = mock(RequestFactory.class);
669         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOff\":true}"))
670                 .thenReturn(request);
671
672         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
673
674         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
675                 new DeviceStateDispatcher(), scheduler)) {
676             webservice.setAccessToken(ACCESS_TOKEN);
677
678             // when:
679             assertThrows(TooManyRequestsException.class, () -> {
680                 webservice.putPowerState(DEVICE_IDENTIFIER, false);
681             });
682         }
683     }
684
685     @Test
686     public void testPutProgramResultsInARequestWithCorrectJson() throws Exception {
687         // given:
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);
692
693         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
694
695         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
696                 new DeviceStateDispatcher(), scheduler)) {
697             webservice.setAccessToken(ACCESS_TOKEN);
698
699             // when:
700             webservice.putProgram(DEVICE_IDENTIFIER, 1);
701
702             // then:
703             verify(request).send();
704             verifyNoMoreInteractions(request);
705         }
706     }
707
708     @Test
709     public void testDispatchDeviceStateIsDelegatedToDeviceStateDispatcher() throws Exception {
710         // given:
711         RequestFactory requestFactory = mock(RequestFactory.class);
712         DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
713         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
714
715         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
716                 dispatcher, scheduler)) {
717             // when:
718             webservice.dispatchDeviceState(DEVICE_IDENTIFIER);
719
720             // then:
721             verify(dispatcher).dispatchDeviceState(DEVICE_IDENTIFIER);
722             verifyNoMoreInteractions(dispatcher);
723         }
724     }
725
726     @Test
727     public void receivingSseActionsEventNotifiesConnectionAlive() throws Exception {
728         // given:
729         var requestFactory = mock(RequestFactory.class);
730         var dispatcher = mock(DeviceStateDispatcher.class);
731         var scheduler = mock(ScheduledExecutorService.class);
732
733         var connectionStatusListener = mock(ConnectionStatusListener.class);
734
735         try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
736                 scheduler)) {
737             webservice.addConnectionStatusListener(connectionStatusListener);
738
739             var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS, "{}");
740
741             // when:
742             webservice.onServerSentEvent(actionsEvent);
743
744             // then:
745             verify(connectionStatusListener).onConnectionAlive();
746         }
747     }
748
749     @Test
750     public void receivingSseActionsEventWithNonJsonPayloadDoesNothing() throws Exception {
751         // given:
752         var requestFactory = mock(RequestFactory.class);
753         var dispatcher = mock(DeviceStateDispatcher.class);
754         var scheduler = mock(ScheduledExecutorService.class);
755
756         try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
757                 scheduler)) {
758             webservice.setAccessToken(ACCESS_TOKEN);
759
760             var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
761                     "{\"" + DEVICE_IDENTIFIER + "\": {}");
762
763             // when:
764             webservice.onServerSentEvent(actionsEvent);
765
766             // then:
767             verifyNoMoreInteractions(dispatcher);
768         }
769     }
770
771     @Test
772     public void receivingSseActionsEventFetchesActionsForADevice() throws Exception {
773         // given:
774         var requestFactory = mock(RequestFactory.class);
775         when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
776
777         var response = createContentResponseMock(200, "{}");
778         when(request.send()).thenReturn(response);
779
780         var dispatcher = mock(DeviceStateDispatcher.class);
781         var scheduler = mock(ScheduledExecutorService.class);
782
783         try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
784                 scheduler)) {
785             webservice.setAccessToken(ACCESS_TOKEN);
786
787             var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
788                     "{\"" + DEVICE_IDENTIFIER + "\": {}}");
789
790             // when:
791             webservice.onServerSentEvent(actionsEvent);
792
793             // then:
794             verify(dispatcher).dispatchActionStateUpdates(eq(DEVICE_IDENTIFIER), any());
795             verifyNoMoreInteractions(dispatcher);
796         }
797     }
798
799     @Test
800     public void receivingSseActionsEventFetchesActionsForMultipleDevices() throws Exception {
801         // given:
802         var otherRequest = mock(Request.class);
803         var otherDeviceIdentifier = "000124430017";
804
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);
809
810         var response = createContentResponseMock(200, "{}");
811         when(request.send()).thenReturn(response);
812         when(otherRequest.send()).thenReturn(response);
813
814         var dispatcher = mock(DeviceStateDispatcher.class);
815         var scheduler = mock(ScheduledExecutorService.class);
816
817         try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
818                 scheduler)) {
819             webservice.setAccessToken(ACCESS_TOKEN);
820
821             var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
822                     "{\"" + DEVICE_IDENTIFIER + "\": {}, \"" + otherDeviceIdentifier + "\": {}}");
823
824             // when:
825             webservice.onServerSentEvent(actionsEvent);
826
827             // then:
828             verify(dispatcher).dispatchActionStateUpdates(eq(DEVICE_IDENTIFIER), any());
829             verify(dispatcher).dispatchActionStateUpdates(eq(otherDeviceIdentifier), any());
830             verifyNoMoreInteractions(dispatcher);
831         }
832     }
833
834     @Test
835     public void whenFetchingActionsAfterReceivingSseActionsEventFailsForADeviceThenNothingHappensForThisDevice()
836             throws Exception {
837         // given:
838         var otherRequest = mock(Request.class);
839         var otherDeviceIdentifier = "000124430017";
840
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);
845
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);
850
851         var dispatcher = mock(DeviceStateDispatcher.class);
852         var scheduler = mock(ScheduledExecutorService.class);
853
854         try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
855                 scheduler)) {
856             webservice.setAccessToken(ACCESS_TOKEN);
857
858             var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
859                     "{\"" + DEVICE_IDENTIFIER + "\": {}, \"" + otherDeviceIdentifier + "\": {}}");
860
861             // when:
862             webservice.onServerSentEvent(actionsEvent);
863
864             // then:
865             verify(dispatcher).dispatchActionStateUpdates(eq(DEVICE_IDENTIFIER), any());
866             verifyNoMoreInteractions(dispatcher);
867         }
868     }
869
870     @Test
871     public void whenFetchingActionsAfterReceivingSseActionsEventFailsBecauseOfTooManyRequestsThenNothingHappens()
872             throws Exception {
873         // given:
874         var requestFactory = mock(RequestFactory.class);
875         when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
876
877         var response = createContentResponseMock(429, "{\"message\": \"Too Many Requests\"}");
878         when(request.send()).thenReturn(response);
879
880         var headerFields = mock(HttpFields.class);
881         when(headerFields.containsKey(anyString())).thenReturn(false);
882         when(response.getHeaders()).thenReturn(headerFields);
883
884         var dispatcher = mock(DeviceStateDispatcher.class);
885         var scheduler = mock(ScheduledExecutorService.class);
886
887         try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
888                 scheduler)) {
889             webservice.setAccessToken(ACCESS_TOKEN);
890
891             var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
892                     "{\"" + DEVICE_IDENTIFIER + "\": {}}");
893
894             // when:
895             webservice.onServerSentEvent(actionsEvent);
896
897             // then:
898             verifyNoMoreInteractions(dispatcher);
899         }
900     }
901
902     @Test
903     public void whenFetchingActionsAfterReceivingSseActionsEventFailsBecauseOfAuthorizationFailedThenThisIsNotifiedToListeners()
904             throws Exception {
905         // given:
906         var requestFactory = mock(RequestFactory.class);
907         when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
908
909         var response = createContentResponseMock(401, "{\"message\": \"Unauthorized\"}");
910         when(request.send()).thenReturn(response);
911
912         var dispatcher = mock(DeviceStateDispatcher.class);
913         var scheduler = mock(ScheduledExecutorService.class);
914
915         var connectionStatusListener = mock(ConnectionStatusListener.class);
916
917         try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
918                 scheduler)) {
919             webservice.addConnectionStatusListener(connectionStatusListener);
920             webservice.setAccessToken(ACCESS_TOKEN);
921
922             var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
923                     "{\"" + DEVICE_IDENTIFIER + "\": {}}");
924
925             // when:
926             webservice.onServerSentEvent(actionsEvent);
927
928             // then:
929             verifyNoMoreInteractions(dispatcher);
930             verify(connectionStatusListener).onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
931         }
932     }
933
934     /**
935      * {@link RetryStrategy} for testing purposes. No exceptions will be catched.
936      *
937      * @author Roland Edelhoff - Initial contribution.
938      */
939     private static class UncatchedRetryStrategy implements RetryStrategy {
940
941         @Override
942         public <@Nullable T> T performRetryableOperation(Supplier<T> operation,
943                 Consumer<Exception> onTransientException) {
944             return operation.get();
945         }
946
947         @Override
948         public void performRetryableOperation(Runnable operation, Consumer<Exception> onTransientException) {
949             operation.run();
950         }
951     }
952 }