]> git.basschouten.com Git - openhab-addons.git/blob
28023aa3becef52dc70314f54f582f26ce043acf
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.core.io.net.http.HttpClientFactory;
47
48 /**
49  * @author Björn Lange - Initial contribution
50  */
51 @NonNullByDefault
52 public class DefaultMieleWebserviceTest {
53     private static final String MESSAGE_INTERNAL_SERVER_ERROR = "{\"message\": \"Internal Server Error\"}";
54     private static final String MESSAGE_SERVICE_UNAVAILABLE = "{\"message\": \"unavailable\"}";
55     private static final String MESSAGE_INVALID_JSON = "{\"abc123: \"äfgh\"}";
56
57     private static final String DEVICE_IDENTIFIER = "000124430016";
58
59     private static final String SERVER_ADDRESS = "https://api.mcs3.miele.com";
60     private static final String ENDPOINT_DEVICES = SERVER_ADDRESS + "/v1/devices/";
61     private static final String ENDPOINT_ACTIONS = ENDPOINT_DEVICES + DEVICE_IDENTIFIER + "/actions";
62     private static final String ENDPOINT_LOGOUT = SERVER_ADDRESS + "/thirdparty/logout";
63
64     private static final String ACCESS_TOKEN = "DE_0123456789abcdef0123456789abcdef";
65
66     private final RetryStrategy retryStrategy = new UncatchedRetryStrategy();
67     private final Request request = mock(Request.class);
68
69     @Test
70     public void testDefaultRetryStrategyIsCombinationOfOneTimeRetryStrategyAndAuthorizationFailedStrategy()
71             throws Exception {
72         // given:
73         HttpClientFactory httpClientFactory = mock(HttpClientFactory.class);
74         when(httpClientFactory.createHttpClient(anyString())).thenReturn(MockUtil.mockHttpClient());
75         LanguageProvider languageProvider = mock(LanguageProvider.class);
76         OAuthTokenRefresher tokenRefresher = mock(OAuthTokenRefresher.class);
77         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
78
79         // when:
80         DefaultMieleWebservice webservice = new DefaultMieleWebservice(MieleWebserviceConfiguration.builder()
81                 .withHttpClientFactory(httpClientFactory).withLanguageProvider(languageProvider)
82                 .withTokenRefresher(tokenRefresher).withServiceHandle(MieleCloudBindingTestConstants.SERVICE_HANDLE)
83                 .withScheduler(scheduler).build());
84
85         // then:
86         RetryStrategy retryStrategy = getPrivate(webservice, "retryStrategy");
87         assertTrue(retryStrategy instanceof RetryStrategyCombiner);
88
89         RetryStrategy first = getPrivate(retryStrategy, "first");
90         assertTrue(first instanceof NTimesRetryStrategy);
91         int numberOfRetries = getPrivate(first, "numberOfRetries");
92         assertEquals(1, numberOfRetries);
93
94         RetryStrategy second = getPrivate(retryStrategy, "second");
95         assertTrue(second instanceof AuthorizationFailedRetryStrategy);
96         OAuthTokenRefresher internalTokenRefresher = getPrivate(second, "tokenRefresher");
97         assertEquals(tokenRefresher, internalTokenRefresher);
98     }
99
100     private ContentResponse createContentResponseMock(int errorCode, String content) {
101         ContentResponse response = mock(ContentResponse.class);
102         when(response.getStatus()).thenReturn(errorCode);
103         when(response.getContentAsString()).thenReturn(content);
104         return response;
105     }
106
107     private void performFetchActions() throws Exception {
108         RequestFactory requestFactory = mock(RequestFactory.class);
109         when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
110
111         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
112
113         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
114                 new DeviceStateDispatcher(), scheduler)) {
115             webservice.setAccessToken(ACCESS_TOKEN);
116
117             webservice.fetchActions(DEVICE_IDENTIFIER);
118         }
119     }
120
121     private void performFetchActionsExpectingFailure(ConnectionError expectedError) throws Exception {
122         try {
123             performFetchActions();
124         } catch (MieleWebserviceException e) {
125             assertEquals(expectedError, e.getConnectionError());
126             throw e;
127         } catch (MieleWebserviceTransientException e) {
128             assertEquals(expectedError, e.getConnectionError());
129             throw e;
130         }
131     }
132
133     @Test
134     public void testTimeoutExceptionWhilePerformingFetchActionsRequest() throws Exception {
135         // given:
136         when(request.send()).thenThrow(TimeoutException.class);
137
138         // when:
139         assertThrows(MieleWebserviceTransientException.class, () -> {
140             performFetchActionsExpectingFailure(ConnectionError.TIMEOUT);
141         });
142     }
143
144     @Test
145     public void test500InternalServerErrorWhilePerformingFetchActionsRequest() throws Exception {
146         // given:
147         ContentResponse contentResponse = createContentResponseMock(500, MESSAGE_INTERNAL_SERVER_ERROR);
148         when(request.send()).thenReturn(contentResponse);
149
150         // when:
151         assertThrows(MieleWebserviceTransientException.class, () -> {
152             performFetchActionsExpectingFailure(ConnectionError.SERVER_ERROR);
153         });
154     }
155
156     @Test
157     public void test503ServiceUnavailableWhilePerformingFetchActionsRequest() throws Exception {
158         // given:
159         ContentResponse contentResponse = createContentResponseMock(503, MESSAGE_SERVICE_UNAVAILABLE);
160         when(request.send()).thenReturn(contentResponse);
161
162         // when:
163         assertThrows(MieleWebserviceTransientException.class, () -> {
164             performFetchActionsExpectingFailure(ConnectionError.SERVICE_UNAVAILABLE);
165         });
166     }
167
168     @Test
169     public void testInvalidJsonWhilePerformingFetchActionsRequest() throws Exception {
170         // given:
171         ContentResponse contentResponse = createContentResponseMock(200, MESSAGE_INVALID_JSON);
172         when(request.send()).thenReturn(contentResponse);
173
174         // when:
175         assertThrows(MieleWebserviceTransientException.class, () -> {
176             performFetchActionsExpectingFailure(ConnectionError.RESPONSE_MALFORMED);
177         });
178     }
179
180     @Test
181     public void testInterruptedExceptionWhilePerformingFetchActionsRequest() throws Exception {
182         // given:
183         when(request.send()).thenThrow(InterruptedException.class);
184
185         // when:
186         assertThrows(MieleWebserviceException.class, () -> {
187             performFetchActionsExpectingFailure(ConnectionError.REQUEST_INTERRUPTED);
188         });
189     }
190
191     @Test
192     public void testExecutionExceptionWhilePerformingFetchActionsRequest() throws Exception {
193         // given:
194         when(request.send()).thenThrow(ExecutionException.class);
195
196         // when:
197         assertThrows(MieleWebserviceException.class, () -> {
198             performFetchActionsExpectingFailure(ConnectionError.REQUEST_EXECUTION_FAILED);
199         });
200     }
201
202     @Test
203     public void test400BadRequestWhilePerformingFetchActionsRequest() throws Exception {
204         // given:
205         ContentResponse response = createContentResponseMock(400, "{\"message\": \"grant_type is invalid\"}");
206         when(request.send()).thenReturn(response);
207
208         // when:
209         assertThrows(MieleWebserviceException.class, () -> {
210             performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
211         });
212     }
213
214     @Test
215     public void test401UnauthorizedWhilePerformingFetchActionsRequest() throws Exception {
216         // given:
217         ContentResponse response = createContentResponseMock(401, "{\"message\": \"Unauthorized\"}");
218         when(request.send()).thenReturn(response);
219
220         // when:
221         assertThrows(AuthorizationFailedException.class, () -> {
222             performFetchActions();
223         });
224     }
225
226     @Test
227     public void test404NotFoundWhilePerformingFetchActionsRequest() throws Exception {
228         // given:
229         ContentResponse response = createContentResponseMock(404, "{\"message\": \"Not found\"}");
230         when(request.send()).thenReturn(response);
231
232         // when:
233         assertThrows(MieleWebserviceException.class, () -> {
234             performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
235         });
236     }
237
238     @Test
239     public void test405MethodNotAllowedWhilePerformingFetchActionsRequest() throws Exception {
240         // given:
241         ContentResponse response = createContentResponseMock(405, "{\"message\": \"HTTP 405 Method Not Allowed\"}");
242         when(request.send()).thenReturn(response);
243
244         // when:
245         assertThrows(MieleWebserviceException.class, () -> {
246             performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
247         });
248     }
249
250     @Test
251     public void test429TooManyRequestsWhilePerformingFetchActionsRequest() throws Exception {
252         // given:
253         HttpFields headerFields = mock(HttpFields.class);
254         when(headerFields.containsKey(anyString())).thenReturn(false);
255
256         ContentResponse response = createContentResponseMock(429, "{\"message\": \"Too Many Requests\"}");
257         when(response.getHeaders()).thenReturn(headerFields);
258
259         when(request.send()).thenReturn(response);
260
261         // when:
262         assertThrows(TooManyRequestsException.class, () -> {
263             performFetchActions();
264         });
265     }
266
267     @Test
268     public void test502BadGatewayWhilePerforminggFetchActionsRequest() throws Exception {
269         // given:
270         ContentResponse response = createContentResponseMock(502, "{\"message\": \"Bad Gateway\"}");
271         when(request.send()).thenReturn(response);
272
273         // when:
274         assertThrows(MieleWebserviceException.class, () -> {
275             performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
276         });
277     }
278
279     @Test
280     public void testMalformatedBodyWhilePerforminggFetchActionsRequest() throws Exception {
281         // given:
282         ContentResponse response = createContentResponseMock(502, "{\"message \"Bad Gateway\"}");
283         when(request.send()).thenReturn(response);
284
285         // when:
286         assertThrows(MieleWebserviceException.class, () -> {
287             performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
288         });
289     }
290
291     private void fillRequestMockWithDefaultContent() throws InterruptedException, TimeoutException, ExecutionException {
292         ContentResponse response = createContentResponseMock(200,
293                 "{\"000124430016\":{\"ident\": {\"deviceName\": \"MyFancyHood\", \"deviceIdentLabel\": {\"fabNumber\": \"000124430016\"}}}}");
294         when(request.send()).thenReturn(response);
295     }
296
297     @Test
298     public void testAddDeviceStateListenerIsDelegatedToDeviceStateDispatcher() throws Exception {
299         // given:
300         RequestFactory requestFactory = mock(RequestFactory.class);
301         DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
302         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
303
304         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
305                 dispatcher, scheduler)) {
306             DeviceStateListener listener = mock(DeviceStateListener.class);
307
308             // when:
309             webservice.addDeviceStateListener(listener);
310
311             // then:
312             verify(dispatcher).addListener(listener);
313             verifyNoMoreInteractions(dispatcher);
314         }
315     }
316
317     @Test
318     public void testFetchActionsDelegatesDeviceStateListenerDispatchingToDeviceStateDispatcher() throws Exception {
319         // given:
320         fillRequestMockWithDefaultContent();
321
322         RequestFactory requestFactory = mock(RequestFactory.class);
323         when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
324
325         DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
326
327         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
328
329         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy, dispatcher,
330                 scheduler)) {
331             webservice.setAccessToken(ACCESS_TOKEN);
332
333             // when:
334             webservice.fetchActions(DEVICE_IDENTIFIER);
335
336             // then:
337             verify(dispatcher).dispatchActionStateUpdates(any(), any());
338             verifyNoMoreInteractions(dispatcher);
339         }
340     }
341
342     @Test
343     public void testFetchActionsThrowsMieleWebserviceTransientExceptionWhenRequestContentIsMalformatted()
344             throws Exception {
345         // given:
346         ContentResponse response = createContentResponseMock(200, "{\"}");
347         when(request.send()).thenReturn(response);
348
349         RequestFactory requestFactory = mock(RequestFactory.class);
350         when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
351
352         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
353
354         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
355                 new DeviceStateDispatcher(), scheduler)) {
356             webservice.setAccessToken(ACCESS_TOKEN);
357
358             // when:
359             assertThrows(MieleWebserviceTransientException.class, () -> {
360                 webservice.fetchActions(DEVICE_IDENTIFIER);
361             });
362         }
363     }
364
365     @Test
366     public void testPutProcessActionSendsRequestWithCorrectJsonContent() throws Exception {
367         // given:
368         ContentResponse response = createContentResponseMock(204, "");
369         when(request.send()).thenReturn(response);
370
371         RequestFactory requestFactory = mock(RequestFactory.class);
372         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"processAction\":1}"))
373                 .thenReturn(request);
374
375         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
376
377         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
378                 new DeviceStateDispatcher(), scheduler)) {
379             webservice.setAccessToken(ACCESS_TOKEN);
380
381             // when:
382             webservice.putProcessAction(DEVICE_IDENTIFIER, ProcessAction.START);
383
384             // then:
385             verify(request).send();
386             verifyNoMoreInteractions(request);
387         }
388     }
389
390     @Test
391     public void testPutProcessActionThrowsIllegalArgumentExceptionWhenProcessActionIsUnknown() throws Exception {
392         // given:
393         RequestFactory requestFactory = mock(RequestFactory.class);
394
395         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
396
397         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
398                 new DeviceStateDispatcher(), scheduler)) {
399
400             // when:
401             assertThrows(IllegalArgumentException.class, () -> {
402                 webservice.putProcessAction(DEVICE_IDENTIFIER, ProcessAction.UNKNOWN);
403             });
404         }
405     }
406
407     @Test
408     public void testPutProcessActionThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
409         // given:
410         HttpFields responseHeaders = mock(HttpFields.class);
411         when(responseHeaders.containsKey(anyString())).thenReturn(false);
412
413         ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
414         when(response.getHeaders()).thenReturn(responseHeaders);
415
416         when(request.send()).thenReturn(response);
417
418         RequestFactory requestFactory = mock(RequestFactory.class);
419         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"processAction\":1}"))
420                 .thenReturn(request);
421
422         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
423
424         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
425                 new DeviceStateDispatcher(), scheduler)) {
426             webservice.setAccessToken(ACCESS_TOKEN);
427
428             // when:
429             assertThrows(TooManyRequestsException.class, () -> {
430                 webservice.putProcessAction(DEVICE_IDENTIFIER, ProcessAction.START);
431             });
432         }
433     }
434
435     @Test
436     public void testPutLightSendsRequestWithCorrectJsonContentWhenTurningTheLightOn() throws Exception {
437         // given:
438         ContentResponse response = createContentResponseMock(204, "");
439         when(request.send()).thenReturn(response);
440
441         RequestFactory requestFactory = mock(RequestFactory.class);
442         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":1}")).thenReturn(request);
443
444         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
445
446         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
447                 new DeviceStateDispatcher(), scheduler)) {
448             webservice.setAccessToken(ACCESS_TOKEN);
449
450             // when:
451             webservice.putLight(DEVICE_IDENTIFIER, true);
452
453             // then:
454             verify(request).send();
455             verifyNoMoreInteractions(request);
456         }
457     }
458
459     @Test
460     public void testPutLightSendsRequestWithCorrectJsonContentWhenTurningTheLightOff() throws Exception {
461         // given:
462         ContentResponse response = createContentResponseMock(204, "");
463         when(request.send()).thenReturn(response);
464
465         RequestFactory requestFactory = mock(RequestFactory.class);
466         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":2}")).thenReturn(request);
467
468         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
469
470         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
471                 new DeviceStateDispatcher(), scheduler)) {
472             webservice.setAccessToken(ACCESS_TOKEN);
473
474             // when:
475             webservice.putLight(DEVICE_IDENTIFIER, false);
476
477             // then:
478             verify(request).send();
479             verifyNoMoreInteractions(request);
480         }
481     }
482
483     @Test
484     public void testPutLightThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
485         // given:
486         HttpFields responseHeaders = mock(HttpFields.class);
487         when(responseHeaders.containsKey(anyString())).thenReturn(false);
488
489         ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
490         when(response.getHeaders()).thenReturn(responseHeaders);
491
492         when(request.send()).thenReturn(response);
493
494         RequestFactory requestFactory = mock(RequestFactory.class);
495         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":2}")).thenReturn(request);
496
497         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
498
499         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
500                 new DeviceStateDispatcher(), scheduler)) {
501             webservice.setAccessToken(ACCESS_TOKEN);
502
503             // when:
504             assertThrows(TooManyRequestsException.class, () -> {
505                 webservice.putLight(DEVICE_IDENTIFIER, false);
506             });
507         }
508     }
509
510     @Test
511     public void testLogoutInvalidatesAccessTokenOnSuccess() throws Exception {
512         // given:
513         ContentResponse response = createContentResponseMock(204, "");
514         when(request.send()).thenReturn(response);
515
516         RequestFactory requestFactory = mock(RequestFactory.class);
517         when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
518
519         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
520
521         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
522                 new DeviceStateDispatcher(), scheduler)) {
523             webservice.setAccessToken(ACCESS_TOKEN);
524
525             // when:
526             webservice.logout();
527
528             // then:
529             assertFalse(webservice.hasAccessToken());
530             verify(request).send();
531             verifyNoMoreInteractions(request);
532         }
533     }
534
535     @Test
536     public void testLogoutThrowsMieleWebserviceExceptionWhenMieleWebserviceTransientExceptionIsThrownInternally()
537             throws Exception {
538         // given:
539         when(request.send()).thenThrow(TimeoutException.class);
540
541         RequestFactory requestFactory = mock(RequestFactory.class);
542         when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
543
544         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
545
546         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
547                 new DeviceStateDispatcher(), scheduler)) {
548             webservice.setAccessToken(ACCESS_TOKEN);
549
550             // when:
551             assertThrows(MieleWebserviceException.class, () -> {
552                 webservice.logout();
553             });
554         }
555     }
556
557     @Test
558     public void testLogoutInvalidatesAccessTokenWhenOperationFails() throws Exception {
559         // given:
560         when(request.send()).thenThrow(TimeoutException.class);
561
562         RequestFactory requestFactory = mock(RequestFactory.class);
563         when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
564
565         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
566
567         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
568                 new DeviceStateDispatcher(), scheduler)) {
569             webservice.setAccessToken(ACCESS_TOKEN);
570
571             // when:
572             try {
573                 webservice.logout();
574             } catch (MieleWebserviceException e) {
575             }
576
577             // then:
578             assertFalse(webservice.hasAccessToken());
579         }
580     }
581
582     @Test
583     public void testRemoveDeviceStateListenerIsDelegatedToDeviceStateDispatcher() throws Exception {
584         // given:
585         RequestFactory requestFactory = mock(RequestFactory.class);
586
587         DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
588
589         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
590
591         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
592                 dispatcher, scheduler)) {
593             DeviceStateListener listener = mock(DeviceStateListener.class);
594             webservice.addDeviceStateListener(listener);
595
596             // when:
597             webservice.removeDeviceStateListener(listener);
598
599             // then:
600             verify(dispatcher).addListener(listener);
601             verify(dispatcher).removeListener(listener);
602             verifyNoMoreInteractions(dispatcher);
603         }
604     }
605
606     @Test
607     public void testPutPowerStateSendsRequestWithCorrectJsonContentWhenSwitchingTheDeviceOn() throws Exception {
608         // given:
609         ContentResponse response = createContentResponseMock(204, "");
610         when(request.send()).thenReturn(response);
611
612         RequestFactory requestFactory = mock(RequestFactory.class);
613         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOn\":true}")).thenReturn(request);
614
615         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
616
617         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
618                 new DeviceStateDispatcher(), scheduler)) {
619             webservice.setAccessToken(ACCESS_TOKEN);
620
621             // when:
622             webservice.putPowerState(DEVICE_IDENTIFIER, true);
623
624             // then:
625             verify(request).send();
626             verifyNoMoreInteractions(request);
627         }
628     }
629
630     @Test
631     public void testPutPowerStateSendsRequestWithCorrectJsonContentWhenDeviceIsSwitchedOff() throws Exception {
632         // given:
633         ContentResponse response = createContentResponseMock(204, "");
634         when(request.send()).thenReturn(response);
635
636         RequestFactory requestFactory = mock(RequestFactory.class);
637         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOff\":true}"))
638                 .thenReturn(request);
639
640         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
641
642         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
643                 new DeviceStateDispatcher(), scheduler)) {
644             webservice.setAccessToken(ACCESS_TOKEN);
645
646             // when:
647             webservice.putPowerState(DEVICE_IDENTIFIER, false);
648
649             // then:
650             verify(request).send();
651             verifyNoMoreInteractions(request);
652         }
653     }
654
655     @Test
656     public void testPutPowerStateThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
657         // given:
658         HttpFields responseHeaders = mock(HttpFields.class);
659         when(responseHeaders.containsKey(anyString())).thenReturn(false);
660
661         ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
662         when(response.getHeaders()).thenReturn(responseHeaders);
663
664         when(request.send()).thenReturn(response);
665
666         RequestFactory requestFactory = mock(RequestFactory.class);
667         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOff\":true}"))
668                 .thenReturn(request);
669
670         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
671
672         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
673                 new DeviceStateDispatcher(), scheduler)) {
674             webservice.setAccessToken(ACCESS_TOKEN);
675
676             // when:
677             assertThrows(TooManyRequestsException.class, () -> {
678                 webservice.putPowerState(DEVICE_IDENTIFIER, false);
679             });
680         }
681     }
682
683     @Test
684     public void testPutProgramResultsInARequestWithCorrectJson() throws Exception {
685         // given:
686         ContentResponse response = createContentResponseMock(204, "");
687         when(request.send()).thenReturn(response);
688         RequestFactory requestFactory = mock(RequestFactory.class);
689         when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"programId\":1}")).thenReturn(request);
690
691         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
692
693         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
694                 new DeviceStateDispatcher(), scheduler)) {
695             webservice.setAccessToken(ACCESS_TOKEN);
696
697             // when:
698             webservice.putProgram(DEVICE_IDENTIFIER, 1);
699
700             // then:
701             verify(request).send();
702             verifyNoMoreInteractions(request);
703         }
704     }
705
706     @Test
707     public void testDispatchDeviceStateIsDelegatedToDeviceStateDispatcher() throws Exception {
708         // given:
709         RequestFactory requestFactory = mock(RequestFactory.class);
710         DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
711         ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
712
713         try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
714                 dispatcher, scheduler)) {
715             // when:
716             webservice.dispatchDeviceState(DEVICE_IDENTIFIER);
717
718             // then:
719             verify(dispatcher).dispatchDeviceState(DEVICE_IDENTIFIER);
720             verifyNoMoreInteractions(dispatcher);
721         }
722     }
723
724     /**
725      * {@link RetryStrategy} for testing purposes. No exceptions will be catched.
726      *
727      * @author Roland Edelhoff - Initial contribution.
728      */
729     private static class UncatchedRetryStrategy implements RetryStrategy {
730
731         @Override
732         public <@Nullable T> T performRetryableOperation(Supplier<T> operation,
733                 Consumer<Exception> onTransientException) {
734             return operation.get();
735         }
736
737         @Override
738         public void performRetryableOperation(Runnable operation, Consumer<Exception> onTransientException) {
739             operation.run();
740         }
741     }
742 }