]> git.basschouten.com Git - openhab-addons.git/blob
4a249353f73c2da84a5ed93d506642bf78c8c62c
[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             // when:
402             assertThrows(IllegalArgumentException.class, () -> {
403                 webservice.putProcessAction(DEVICE_IDENTIFIER, ProcessAction.UNKNOWN);
404             });
405         }
406     }
407
408     @Test
409     public void testPutProcessActionThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
410         // given:
411         HttpFields responseHeaders = mock(HttpFields.class);
412         when(responseHeaders.containsKey(anyString())).thenReturn(false);
413
414         ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
415         when(response.getHeaders()).thenReturn(responseHeaders);
416
417         when(request.send()).thenReturn(response);
418
419         RequestFactory requestFactory = mock(RequestFactory.class);
420         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"processAction\":1}"))
421                 .thenReturn(request);
422
423         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
424
425         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
426                 new DeviceStateDispatcher(), scheduler)) {
427             webservice.setAccessToken(ACCESS_TOKEN);
428
429             // when:
430             assertThrows(TooManyRequestsException.class, () -> {
431                 webservice.putProcessAction(DEVICE_IDENTIFIER, ProcessAction.START);
432             });
433         }
434     }
435
436     @Test
437     public void testPutLightSendsRequestWithCorrectJsonContentWhenTurningTheLightOn() throws Exception {
438         // given:
439         ContentResponse response = createContentResponseMock(204, "");
440         when(request.send()).thenReturn(response);
441
442         RequestFactory requestFactory = mock(RequestFactory.class);
443         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":1}")).thenReturn(request);
444
445         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
446
447         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
448                 new DeviceStateDispatcher(), scheduler)) {
449             webservice.setAccessToken(ACCESS_TOKEN);
450
451             // when:
452             webservice.putLight(DEVICE_IDENTIFIER, true);
453
454             // then:
455             verify(request).send();
456             verifyNoMoreInteractions(request);
457         }
458     }
459
460     @Test
461     public void testPutLightSendsRequestWithCorrectJsonContentWhenTurningTheLightOff() throws Exception {
462         // given:
463         ContentResponse response = createContentResponseMock(204, "");
464         when(request.send()).thenReturn(response);
465
466         RequestFactory requestFactory = mock(RequestFactory.class);
467         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":2}")).thenReturn(request);
468
469         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
470
471         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
472                 new DeviceStateDispatcher(), scheduler)) {
473             webservice.setAccessToken(ACCESS_TOKEN);
474
475             // when:
476             webservice.putLight(DEVICE_IDENTIFIER, false);
477
478             // then:
479             verify(request).send();
480             verifyNoMoreInteractions(request);
481         }
482     }
483
484     @Test
485     public void testPutLightThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
486         // given:
487         HttpFields responseHeaders = mock(HttpFields.class);
488         when(responseHeaders.containsKey(anyString())).thenReturn(false);
489
490         ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
491         when(response.getHeaders()).thenReturn(responseHeaders);
492
493         when(request.send()).thenReturn(response);
494
495         RequestFactory requestFactory = mock(RequestFactory.class);
496         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":2}")).thenReturn(request);
497
498         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
499
500         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
501                 new DeviceStateDispatcher(), scheduler)) {
502             webservice.setAccessToken(ACCESS_TOKEN);
503
504             // when:
505             assertThrows(TooManyRequestsException.class, () -> {
506                 webservice.putLight(DEVICE_IDENTIFIER, false);
507             });
508         }
509     }
510
511     @Test
512     public void testLogoutInvalidatesAccessTokenOnSuccess() throws Exception {
513         // given:
514         ContentResponse response = createContentResponseMock(204, "");
515         when(request.send()).thenReturn(response);
516
517         RequestFactory requestFactory = mock(RequestFactory.class);
518         when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
519
520         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
521
522         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
523                 new DeviceStateDispatcher(), scheduler)) {
524             webservice.setAccessToken(ACCESS_TOKEN);
525
526             // when:
527             webservice.logout();
528
529             // then:
530             assertFalse(webservice.hasAccessToken());
531             verify(request).send();
532             verifyNoMoreInteractions(request);
533         }
534     }
535
536     @Test
537     public void testLogoutThrowsMieleWebserviceExceptionWhenMieleWebserviceTransientExceptionIsThrownInternally()
538             throws Exception {
539         // given:
540         when(request.send()).thenThrow(TimeoutException.class);
541
542         RequestFactory requestFactory = mock(RequestFactory.class);
543         when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
544
545         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
546
547         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
548                 new DeviceStateDispatcher(), scheduler)) {
549             webservice.setAccessToken(ACCESS_TOKEN);
550
551             // when:
552             assertThrows(MieleWebserviceException.class, () -> {
553                 webservice.logout();
554             });
555         }
556     }
557
558     @Test
559     public void testLogoutInvalidatesAccessTokenWhenOperationFails() throws Exception {
560         // given:
561         when(request.send()).thenThrow(TimeoutException.class);
562
563         RequestFactory requestFactory = mock(RequestFactory.class);
564         when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
565
566         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
567
568         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
569                 new DeviceStateDispatcher(), scheduler)) {
570             webservice.setAccessToken(ACCESS_TOKEN);
571
572             // when:
573             try {
574                 webservice.logout();
575             } catch (MieleWebserviceException e) {
576             }
577
578             // then:
579             assertFalse(webservice.hasAccessToken());
580         }
581     }
582
583     @Test
584     public void testRemoveDeviceStateListenerIsDelegatedToDeviceStateDispatcher() throws Exception {
585         // given:
586         RequestFactory requestFactory = mock(RequestFactory.class);
587
588         DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
589
590         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
591
592         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
593                 dispatcher, scheduler)) {
594             DeviceStateListener listener = mock(DeviceStateListener.class);
595             webservice.addDeviceStateListener(listener);
596
597             // when:
598             webservice.removeDeviceStateListener(listener);
599
600             // then:
601             verify(dispatcher).addListener(listener);
602             verify(dispatcher).removeListener(listener);
603             verifyNoMoreInteractions(dispatcher);
604         }
605     }
606
607     @Test
608     public void testPutPowerStateSendsRequestWithCorrectJsonContentWhenSwitchingTheDeviceOn() throws Exception {
609         // given:
610         ContentResponse response = createContentResponseMock(204, "");
611         when(request.send()).thenReturn(response);
612
613         RequestFactory requestFactory = mock(RequestFactory.class);
614         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOn\":true}")).thenReturn(request);
615
616         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
617
618         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
619                 new DeviceStateDispatcher(), scheduler)) {
620             webservice.setAccessToken(ACCESS_TOKEN);
621
622             // when:
623             webservice.putPowerState(DEVICE_IDENTIFIER, true);
624
625             // then:
626             verify(request).send();
627             verifyNoMoreInteractions(request);
628         }
629     }
630
631     @Test
632     public void testPutPowerStateSendsRequestWithCorrectJsonContentWhenDeviceIsSwitchedOff() throws Exception {
633         // given:
634         ContentResponse response = createContentResponseMock(204, "");
635         when(request.send()).thenReturn(response);
636
637         RequestFactory requestFactory = mock(RequestFactory.class);
638         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOff\":true}"))
639                 .thenReturn(request);
640
641         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
642
643         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
644                 new DeviceStateDispatcher(), scheduler)) {
645             webservice.setAccessToken(ACCESS_TOKEN);
646
647             // when:
648             webservice.putPowerState(DEVICE_IDENTIFIER, false);
649
650             // then:
651             verify(request).send();
652             verifyNoMoreInteractions(request);
653         }
654     }
655
656     @Test
657     public void testPutPowerStateThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
658         // given:
659         HttpFields responseHeaders = mock(HttpFields.class);
660         when(responseHeaders.containsKey(anyString())).thenReturn(false);
661
662         ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
663         when(response.getHeaders()).thenReturn(responseHeaders);
664
665         when(request.send()).thenReturn(response);
666
667         RequestFactory requestFactory = mock(RequestFactory.class);
668         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOff\":true}"))
669                 .thenReturn(request);
670
671         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
672
673         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
674                 new DeviceStateDispatcher(), scheduler)) {
675             webservice.setAccessToken(ACCESS_TOKEN);
676
677             // when:
678             assertThrows(TooManyRequestsException.class, () -> {
679                 webservice.putPowerState(DEVICE_IDENTIFIER, false);
680             });
681         }
682     }
683
684     @Test
685     public void testPutProgramResultsInARequestWithCorrectJson() throws Exception {
686         // given:
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);
691
692         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
693
694         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
695                 new DeviceStateDispatcher(), scheduler)) {
696             webservice.setAccessToken(ACCESS_TOKEN);
697
698             // when:
699             webservice.putProgram(DEVICE_IDENTIFIER, 1);
700
701             // then:
702             verify(request).send();
703             verifyNoMoreInteractions(request);
704         }
705     }
706
707     @Test
708     public void testDispatchDeviceStateIsDelegatedToDeviceStateDispatcher() throws Exception {
709         // given:
710         RequestFactory requestFactory = mock(RequestFactory.class);
711         DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
712         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
713
714         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
715                 dispatcher, scheduler)) {
716             // when:
717             webservice.dispatchDeviceState(DEVICE_IDENTIFIER);
718
719             // then:
720             verify(dispatcher).dispatchDeviceState(DEVICE_IDENTIFIER);
721             verifyNoMoreInteractions(dispatcher);
722         }
723     }
724
725     @Test
726     public void receivingSseActionsEventNotifiesConnectionAlive() throws Exception {
727         // given:
728         var requestFactory = mock(RequestFactory.class);
729         var dispatcher = mock(DeviceStateDispatcher.class);
730         var scheduler = mock(ScheduledExecutorService.class);
731
732         var connectionStatusListener = mock(ConnectionStatusListener.class);
733
734         try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
735                 scheduler)) {
736             webservice.addConnectionStatusListener(connectionStatusListener);
737
738             var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS, "{}");
739
740             // when:
741             webservice.onServerSentEvent(actionsEvent);
742
743             // then:
744             verify(connectionStatusListener).onConnectionAlive();
745         }
746     }
747
748     @Test
749     public void receivingSseActionsEventWithNonJsonPayloadDoesNothing() throws Exception {
750         // given:
751         var requestFactory = mock(RequestFactory.class);
752         var dispatcher = mock(DeviceStateDispatcher.class);
753         var scheduler = mock(ScheduledExecutorService.class);
754
755         try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
756                 scheduler)) {
757             webservice.setAccessToken(ACCESS_TOKEN);
758
759             var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
760                     "{\"" + DEVICE_IDENTIFIER + "\": {}");
761
762             // when:
763             webservice.onServerSentEvent(actionsEvent);
764
765             // then:
766             verifyNoMoreInteractions(dispatcher);
767         }
768     }
769
770     @Test
771     public void receivingSseActionsEventFetchesActionsForADevice() throws Exception {
772         // given:
773         var requestFactory = mock(RequestFactory.class);
774         when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
775
776         var response = createContentResponseMock(200, "{}");
777         when(request.send()).thenReturn(response);
778
779         var dispatcher = mock(DeviceStateDispatcher.class);
780         var scheduler = mock(ScheduledExecutorService.class);
781
782         try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
783                 scheduler)) {
784             webservice.setAccessToken(ACCESS_TOKEN);
785
786             var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
787                     "{\"" + DEVICE_IDENTIFIER + "\": {}}");
788
789             // when:
790             webservice.onServerSentEvent(actionsEvent);
791
792             // then:
793             verify(dispatcher).dispatchActionStateUpdates(eq(DEVICE_IDENTIFIER), any());
794             verifyNoMoreInteractions(dispatcher);
795         }
796     }
797
798     @Test
799     public void receivingSseActionsEventFetchesActionsForMultipleDevices() throws Exception {
800         // given:
801         var otherRequest = mock(Request.class);
802         var otherDeviceIdentifier = "000124430017";
803
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);
808
809         var response = createContentResponseMock(200, "{}");
810         when(request.send()).thenReturn(response);
811         when(otherRequest.send()).thenReturn(response);
812
813         var dispatcher = mock(DeviceStateDispatcher.class);
814         var scheduler = mock(ScheduledExecutorService.class);
815
816         try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
817                 scheduler)) {
818             webservice.setAccessToken(ACCESS_TOKEN);
819
820             var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
821                     "{\"" + DEVICE_IDENTIFIER + "\": {}, \"" + otherDeviceIdentifier + "\": {}}");
822
823             // when:
824             webservice.onServerSentEvent(actionsEvent);
825
826             // then:
827             verify(dispatcher).dispatchActionStateUpdates(eq(DEVICE_IDENTIFIER), any());
828             verify(dispatcher).dispatchActionStateUpdates(eq(otherDeviceIdentifier), any());
829             verifyNoMoreInteractions(dispatcher);
830         }
831     }
832
833     @Test
834     public void whenFetchingActionsAfterReceivingSseActionsEventFailsForADeviceThenNothingHappensForThisDevice()
835             throws Exception {
836         // given:
837         var otherRequest = mock(Request.class);
838         var otherDeviceIdentifier = "000124430017";
839
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);
844
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);
849
850         var dispatcher = mock(DeviceStateDispatcher.class);
851         var scheduler = mock(ScheduledExecutorService.class);
852
853         try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
854                 scheduler)) {
855             webservice.setAccessToken(ACCESS_TOKEN);
856
857             var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
858                     "{\"" + DEVICE_IDENTIFIER + "\": {}, \"" + otherDeviceIdentifier + "\": {}}");
859
860             // when:
861             webservice.onServerSentEvent(actionsEvent);
862
863             // then:
864             verify(dispatcher).dispatchActionStateUpdates(eq(DEVICE_IDENTIFIER), any());
865             verifyNoMoreInteractions(dispatcher);
866         }
867     }
868
869     @Test
870     public void whenFetchingActionsAfterReceivingSseActionsEventFailsBecauseOfTooManyRequestsThenNothingHappens()
871             throws Exception {
872         // given:
873         var requestFactory = mock(RequestFactory.class);
874         when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
875
876         var response = createContentResponseMock(429, "{\"message\": \"Too Many Requests\"}");
877         when(request.send()).thenReturn(response);
878
879         var headerFields = mock(HttpFields.class);
880         when(headerFields.containsKey(anyString())).thenReturn(false);
881         when(response.getHeaders()).thenReturn(headerFields);
882
883         var dispatcher = mock(DeviceStateDispatcher.class);
884         var scheduler = mock(ScheduledExecutorService.class);
885
886         try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
887                 scheduler)) {
888             webservice.setAccessToken(ACCESS_TOKEN);
889
890             var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
891                     "{\"" + DEVICE_IDENTIFIER + "\": {}}");
892
893             // when:
894             webservice.onServerSentEvent(actionsEvent);
895
896             // then:
897             verifyNoMoreInteractions(dispatcher);
898         }
899     }
900
901     @Test
902     public void whenFetchingActionsAfterReceivingSseActionsEventFailsBecauseOfAuthorizationFailedThenThisIsNotifiedToListeners()
903             throws Exception {
904         // given:
905         var requestFactory = mock(RequestFactory.class);
906         when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
907
908         var response = createContentResponseMock(401, "{\"message\": \"Unauthorized\"}");
909         when(request.send()).thenReturn(response);
910
911         var dispatcher = mock(DeviceStateDispatcher.class);
912         var scheduler = mock(ScheduledExecutorService.class);
913
914         var connectionStatusListener = mock(ConnectionStatusListener.class);
915
916         try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
917                 scheduler)) {
918             webservice.addConnectionStatusListener(connectionStatusListener);
919             webservice.setAccessToken(ACCESS_TOKEN);
920
921             var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
922                     "{\"" + DEVICE_IDENTIFIER + "\": {}}");
923
924             // when:
925             webservice.onServerSentEvent(actionsEvent);
926
927             // then:
928             verifyNoMoreInteractions(dispatcher);
929             verify(connectionStatusListener).onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
930         }
931     }
932
933     /**
934      * {@link RetryStrategy} for testing purposes. No exceptions will be catched.
935      *
936      * @author Roland Edelhoff - Initial contribution.
937      */
938     private static class UncatchedRetryStrategy implements RetryStrategy {
939
940         @Override
941         public <@Nullable T> T performRetryableOperation(Supplier<T> operation,
942                 Consumer<Exception> onTransientException) {
943             return operation.get();
944         }
945
946         @Override
947         public void performRetryableOperation(Runnable operation, Consumer<Exception> onTransientException) {
948             operation.run();
949         }
950     }
951 }