]> git.basschouten.com Git - openhab-addons.git/blob
d25a832d0c31043342ddff27690f0dd8c97f6958
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.feed.test;
14
15 import static java.lang.Thread.sleep;
16 import static org.hamcrest.CoreMatchers.*;
17 import static org.hamcrest.MatcherAssert.assertThat;
18
19 import java.io.IOException;
20 import java.math.BigDecimal;
21 import java.nio.charset.StandardCharsets;
22
23 import javax.servlet.ServletException;
24 import javax.servlet.http.HttpServlet;
25 import javax.servlet.http.HttpServletRequest;
26 import javax.servlet.http.HttpServletResponse;
27
28 import org.eclipse.jetty.http.HttpStatus;
29 import org.junit.jupiter.api.AfterEach;
30 import org.junit.jupiter.api.BeforeEach;
31 import org.junit.jupiter.api.Test;
32 import org.openhab.binding.feed.internal.FeedBindingConstants;
33 import org.openhab.binding.feed.internal.handler.FeedHandler;
34 import org.openhab.core.config.core.Configuration;
35 import org.openhab.core.items.Item;
36 import org.openhab.core.items.ItemRegistry;
37 import org.openhab.core.items.StateChangeListener;
38 import org.openhab.core.library.items.StringItem;
39 import org.openhab.core.library.types.StringType;
40 import org.openhab.core.test.java.JavaOSGiTest;
41 import org.openhab.core.test.storage.VolatileStorageService;
42 import org.openhab.core.thing.Channel;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.ManagedThingProvider;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingProvider;
47 import org.openhab.core.thing.ThingRegistry;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.ThingUID;
51 import org.openhab.core.thing.binding.builder.ChannelBuilder;
52 import org.openhab.core.thing.binding.builder.ThingBuilder;
53 import org.openhab.core.thing.link.ItemChannelLink;
54 import org.openhab.core.thing.link.ManagedItemChannelLinkProvider;
55 import org.openhab.core.types.RefreshType;
56 import org.openhab.core.types.State;
57 import org.osgi.service.http.HttpService;
58 import org.osgi.service.http.NamespaceException;
59
60 /**
61  * Tests for {@link FeedHandler}
62  *
63  * @author Svilen Valkanov - Initial contribution
64  * @author Wouter Born - Migrate Groovy to Java tests
65  */
66 public class FeedHandlerTest extends JavaOSGiTest {
67
68     // Servlet URL configuration
69     private static final String MOCK_SERVLET_PROTOCOL = "http";
70     private static final String MOCK_SERVLET_HOSTNAME = "localhost";
71     private static final int MOCK_SERVLET_PORT = Integer.getInteger("org.osgi.service.http.port", 8080);
72     private static final String MOCK_SERVLET_PATH = "/test/feed";
73
74     // Files used for the test as input. They are located in /src/test/resources directory
75     /**
76      * The default mock content in the test is RSS 2.0 format, as this is the most popular format
77      */
78     private static final String DEFAULT_MOCK_CONTENT = "rss_2.0.xml";
79
80     /**
81      * One new entry is added to {@link #DEFAULT_MOCK_CONTENT}
82      */
83     private static final String MOCK_CONTENT_CHANGED = "rss_2.0_changed.xml";
84
85     private static final String ITEM_NAME = "testItem";
86     private static final String THING_NAME = "testFeedThing";
87
88     /**
89      * Default auto refresh interval for the test is 1 Minute.
90      */
91     private static final int DEFAULT_TEST_AUTOREFRESH_TIME = 1;
92
93     /**
94      * It is updated from mocked {@link StateChangeListener#stateUpdated() }
95      */
96     private StringType currentItemState;
97
98     // Required services for the test
99     private ManagedThingProvider managedThingProvider;
100     private VolatileStorageService volatileStorageService;
101     private ThingRegistry thingRegistry;
102
103     private FeedServiceMock servlet;
104     private Thing feedThing;
105     private FeedHandler feedHandler;
106     private ChannelUID channelUID;
107     private HttpService httpService;
108
109     /**
110      * This class is used as a mock for HTTP web server, serving XML feed content.
111      */
112     class FeedServiceMock extends HttpServlet {
113         private static final long serialVersionUID = -7810045624309790473L;
114
115         String feedContent;
116         int httpStatus;
117
118         public FeedServiceMock(String feedContentFile) {
119             super();
120             try {
121                 setFeedContent(feedContentFile);
122             } catch (IOException e) {
123                 throw new IllegalArgumentException("Error loading feed content from: " + feedContentFile);
124             }
125             // By default the servlet returns HTTP Status code 200 OK
126             this.httpStatus = HttpStatus.OK_200;
127         }
128
129         @Override
130         protected void doGet(HttpServletRequest request, HttpServletResponse response)
131                 throws ServletException, IOException {
132             response.getOutputStream().println(feedContent);
133             // Recommended RSS MIME type - http://www.rssboard.org/rss-mime-type-application.txt
134             // Atom MIME type is - application/atom+xml
135             // Other MIME types - text/plan, text/xml, text/html are tested and accepted as well
136             response.setContentType("application/rss+xml");
137             response.setStatus(httpStatus);
138         }
139
140         public void setFeedContent(String feedContentFile) throws IOException {
141             String path = "input/" + feedContentFile;
142             feedContent = new String(getClass().getClassLoader().getResourceAsStream(path).readAllBytes(),
143                     StandardCharsets.UTF_8);
144         }
145     }
146
147     @BeforeEach
148     public void setUp() {
149         volatileStorageService = new VolatileStorageService();
150         registerService(volatileStorageService);
151
152         managedThingProvider = getService(ThingProvider.class, ManagedThingProvider.class);
153         assertThat(managedThingProvider, is(notNullValue()));
154
155         thingRegistry = getService(ThingRegistry.class);
156         assertThat(thingRegistry, is(notNullValue()));
157
158         registerFeedTestServlet();
159     }
160
161     @AfterEach
162     public void tearDown() {
163         currentItemState = null;
164         if (feedThing != null) {
165             // Remove the feed thing. The handler will be also disposed automatically
166             Thing removedThing = thingRegistry.forceRemove(feedThing.getUID());
167             assertThat("The feed thing cannot be deleted", removedThing, is(notNullValue()));
168         }
169
170         unregisterFeedTestServlet();
171
172         // Wait for FeedHandler to be unregistered
173         waitForAssert(() -> {
174             feedHandler = (FeedHandler) feedThing.getHandler();
175             assertThat(feedHandler, is(nullValue()));
176         });
177     }
178
179     private synchronized void registerFeedTestServlet() {
180         waitForAssert(() -> assertThat(httpService = getService(HttpService.class), is(notNullValue())));
181         servlet = new FeedServiceMock(DEFAULT_MOCK_CONTENT);
182         try {
183             httpService.registerServlet(MOCK_SERVLET_PATH, servlet, null, null);
184         } catch (ServletException | NamespaceException e) {
185             throw new IllegalStateException("Failed to register feed test servlet", e);
186         }
187     }
188
189     private synchronized void unregisterFeedTestServlet() {
190         waitForAssert(() -> assertThat(httpService = getService(HttpService.class), is(notNullValue())));
191         try {
192             httpService.unregister(MOCK_SERVLET_PATH);
193         } catch (IllegalArgumentException ignore) {
194         }
195         servlet = null;
196     }
197
198     private String generateURLString(String protocol, String hostname, int port, String path) {
199         return protocol + "://" + hostname + ":" + port + path;
200     }
201
202     private void initializeDefaultFeedHandler() {
203         String mockServletURL = generateURLString(MOCK_SERVLET_PROTOCOL, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
204                 MOCK_SERVLET_PATH);
205         // One minute update time is used for the tests
206         BigDecimal defaultTestRefreshInterval = new BigDecimal(DEFAULT_TEST_AUTOREFRESH_TIME);
207         initializeFeedHandler(mockServletURL, defaultTestRefreshInterval);
208     }
209
210     private void initializeFeedHandler(String url) {
211         initializeFeedHandler(url, null);
212     }
213
214     private void initializeFeedHandler(String url, BigDecimal refreshTime) {
215         // Set up configuration
216         Configuration configuration = new Configuration();
217         configuration.put((FeedBindingConstants.URL), url);
218         configuration.put((FeedBindingConstants.REFRESH_TIME), refreshTime);
219
220         // Create Feed Thing
221         ThingUID feedUID = new ThingUID(FeedBindingConstants.FEED_THING_TYPE_UID, THING_NAME);
222         channelUID = new ChannelUID(feedUID, FeedBindingConstants.CHANNEL_LATEST_DESCRIPTION);
223         Channel channel = ChannelBuilder.create(channelUID, "String").build();
224         feedThing = ThingBuilder.create(FeedBindingConstants.FEED_THING_TYPE_UID, feedUID)
225                 .withConfiguration(configuration).withChannel(channel).build();
226
227         managedThingProvider.add(feedThing);
228
229         // Wait for FeedHandler to be registered
230         waitForAssert(() -> {
231             feedHandler = (FeedHandler) feedThing.getHandler();
232             assertThat("FeedHandler is not registered", feedHandler, is(notNullValue()));
233         });
234
235         // This will ensure that the configuration is read before the channelLinked() method in FeedHandler is called !
236         waitForAssert(() -> {
237             assertThat(feedThing.getStatus(), anyOf(is(ThingStatus.ONLINE), is(ThingStatus.OFFLINE)));
238         }, 60000, DFL_SLEEP_TIME);
239         initializeItem(channelUID);
240     }
241
242     private void initializeItem(ChannelUID channelUID) {
243         // Create new item
244         ItemRegistry itemRegistry = getService(ItemRegistry.class);
245         assertThat(itemRegistry, is(notNullValue()));
246
247         StringItem newItem = new StringItem(ITEM_NAME);
248
249         // Add item state change listener
250         StateChangeListener updateListener = new StateChangeListener() {
251             @Override
252             public void stateChanged(Item item, State oldState, State newState) {
253             }
254
255             @Override
256             public void stateUpdated(Item item, State state) {
257                 currentItemState = (StringType) state;
258             }
259         };
260
261         newItem.addStateChangeListener(updateListener);
262         itemRegistry.add(newItem);
263
264         // Add item channel link
265         ManagedItemChannelLinkProvider itemChannelLinkProvider = getService(ManagedItemChannelLinkProvider.class);
266         assertThat(itemChannelLinkProvider, is(notNullValue()));
267         itemChannelLinkProvider.add(new ItemChannelLink(ITEM_NAME, channelUID));
268     }
269
270     private void testIfItemStateIsUpdated(boolean commandReceived, boolean contentChanged)
271             throws IOException, InterruptedException {
272         initializeDefaultFeedHandler();
273
274         waitForAssert(() -> {
275             assertThat("Feed Thing can not be initialized", feedThing.getStatus(), is(equalTo(ThingStatus.ONLINE)));
276             assertThat("Item's state is not updated on initialize", currentItemState, is(notNullValue()));
277         });
278
279         assertThat(currentItemState, is(instanceOf(StringType.class)));
280         StringType firstItemState = currentItemState;
281
282         if (contentChanged) {
283             // The content on the mocked server should be changed
284             servlet.setFeedContent(MOCK_CONTENT_CHANGED);
285         }
286
287         if (commandReceived) {
288             // Before this time has expired, the refresh command will no trigger a request to the server
289             sleep(FeedBindingConstants.MINIMUM_REFRESH_TIME);
290
291             feedHandler.handleCommand(channelUID, RefreshType.REFRESH);
292         } else {
293             // The auto refresh task will handle the update after the default wait time
294             sleep(DEFAULT_TEST_AUTOREFRESH_TIME * 60 * 1000);
295         }
296
297         waitForAssert(() -> {
298             assertThat("Error occurred while trying to connect to server. Content is not downloaded!",
299                     feedThing.getStatus(), is(equalTo(ThingStatus.ONLINE)));
300         });
301
302         waitForAssert(() -> {
303             if (contentChanged) {
304                 assertThat("Content is not updated!", currentItemState, not(equalTo(firstItemState)));
305             } else {
306                 assertThat(currentItemState, is(equalTo(firstItemState)));
307             }
308         });
309     }
310
311     @Test
312     public void assertThatInvalidConfigurationFallsBackToDefaultValues() {
313         String mockServletURL = generateURLString(MOCK_SERVLET_PROTOCOL, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
314                 MOCK_SERVLET_PATH);
315         BigDecimal defaultTestRefreshInterval = new BigDecimal(-10);
316         initializeFeedHandler(mockServletURL, defaultTestRefreshInterval);
317     }
318
319     @Test
320     public void assertThatItemsStateIsNotUpdatedOnAutoRefreshIfContentIsNotChanged()
321             throws IOException, InterruptedException {
322         boolean commandReceived = false;
323         boolean contentChanged = false;
324         testIfItemStateIsUpdated(commandReceived, contentChanged);
325     }
326
327     @Test
328     public void assertThatItemsStateIsUpdatedOnAutoRefreshIfContentChanged() throws IOException, InterruptedException {
329         boolean commandReceived = false;
330         boolean contentChanged = true;
331         testIfItemStateIsUpdated(commandReceived, contentChanged);
332     }
333
334     @Test
335     public void assertThatThingsStatusIsUpdatedWhenHTTP500ErrorCodeIsReceived() throws InterruptedException {
336         testIfThingStatusIsUpdated(HttpStatus.INTERNAL_SERVER_ERROR_500);
337     }
338
339     @Test
340     public void assertThatThingsStatusIsUpdatedWhenHTTP401ErrorCodeIsReceived() throws InterruptedException {
341         testIfThingStatusIsUpdated(HttpStatus.UNAUTHORIZED_401);
342     }
343
344     @Test
345     public void assertThatThingsStatusIsUpdatedWhenHTTP403ErrorCodeIsReceived() throws InterruptedException {
346         testIfThingStatusIsUpdated(HttpStatus.FORBIDDEN_403);
347     }
348
349     @Test
350     public void assertThatThingsStatusIsUpdatedWhenHTTP404ErrorCodeIsReceived() throws InterruptedException {
351         testIfThingStatusIsUpdated(HttpStatus.NOT_FOUND_404);
352     }
353
354     private void testIfThingStatusIsUpdated(Integer serverStatus) throws InterruptedException {
355         initializeDefaultFeedHandler();
356
357         servlet.httpStatus = serverStatus;
358
359         // Before this time has expired, the refresh command will no trigger a request to the server
360         sleep(FeedBindingConstants.MINIMUM_REFRESH_TIME);
361
362         // Invalid channel UID is used for the test, because otherwise
363         feedHandler.handleCommand(channelUID, RefreshType.REFRESH);
364
365         waitForAssert(() -> {
366             assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
367         });
368
369         servlet.httpStatus = HttpStatus.OK_200;
370
371         // Before this time has expired, the refresh command will no trigger a request to the server
372         sleep(FeedBindingConstants.MINIMUM_REFRESH_TIME);
373
374         feedHandler.handleCommand(channelUID, RefreshType.REFRESH);
375
376         waitForAssert(() -> {
377             assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.ONLINE)));
378         });
379     }
380
381     @Test
382     public void createThingWithInvalidUrlProtocol() {
383         String invalidProtocol = "gdfs";
384         String invalidURL = generateURLString(invalidProtocol, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
385                 MOCK_SERVLET_PATH);
386
387         initializeFeedHandler(invalidURL);
388         waitForAssert(() -> {
389             assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
390             assertThat(feedThing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
391         });
392     }
393
394     @Test
395     public void createThingWithInvalidUrlHostname() {
396         String invalidHostname = "invalidhost";
397         String invalidURL = generateURLString(MOCK_SERVLET_PROTOCOL, invalidHostname, MOCK_SERVLET_PORT,
398                 MOCK_SERVLET_PATH);
399
400         initializeFeedHandler(invalidURL);
401         waitForAssert(() -> {
402             assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
403             assertThat(feedThing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.COMMUNICATION_ERROR)));
404         }, 30000, DFL_SLEEP_TIME);
405     }
406
407     @Test
408     public void createThingWithInvalidUrlPath() {
409         String invalidPath = "/invalid/path";
410         String invalidURL = generateURLString(MOCK_SERVLET_PROTOCOL, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
411                 invalidPath);
412
413         initializeFeedHandler(invalidURL);
414         waitForAssert(() -> {
415             assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
416             assertThat(feedThing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.COMMUNICATION_ERROR)));
417         });
418     }
419 }