]> git.basschouten.com Git - openhab-addons.git/blob
1e3d250152a59b5f342ccb49ba6b37bd24c7b9b9
[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         httpService.unregister(MOCK_SERVLET_PATH);
192         servlet = null;
193     }
194
195     private String generateURLString(String protocol, String hostname, int port, String path) {
196         return protocol + "://" + hostname + ":" + port + path;
197     }
198
199     private void initializeDefaultFeedHandler() {
200         String mockServletURL = generateURLString(MOCK_SERVLET_PROTOCOL, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
201                 MOCK_SERVLET_PATH);
202         // One minute update time is used for the tests
203         BigDecimal defaultTestRefreshInterval = new BigDecimal(DEFAULT_TEST_AUTOREFRESH_TIME);
204         initializeFeedHandler(mockServletURL, defaultTestRefreshInterval);
205     }
206
207     private void initializeFeedHandler(String url) {
208         initializeFeedHandler(url, null);
209     }
210
211     private void initializeFeedHandler(String url, BigDecimal refreshTime) {
212         // Set up configuration
213         Configuration configuration = new Configuration();
214         configuration.put((FeedBindingConstants.URL), url);
215         configuration.put((FeedBindingConstants.REFRESH_TIME), refreshTime);
216
217         // Create Feed Thing
218         ThingUID feedUID = new ThingUID(FeedBindingConstants.FEED_THING_TYPE_UID, THING_NAME);
219         channelUID = new ChannelUID(feedUID, FeedBindingConstants.CHANNEL_LATEST_DESCRIPTION);
220         Channel channel = ChannelBuilder.create(channelUID, "String").build();
221         feedThing = ThingBuilder.create(FeedBindingConstants.FEED_THING_TYPE_UID, feedUID)
222                 .withConfiguration(configuration).withChannel(channel).build();
223
224         managedThingProvider.add(feedThing);
225
226         // Wait for FeedHandler to be registered
227         waitForAssert(() -> {
228             feedHandler = (FeedHandler) feedThing.getHandler();
229             assertThat("FeedHandler is not registered", feedHandler, is(notNullValue()));
230         });
231
232         // This will ensure that the configuration is read before the channelLinked() method in FeedHandler is called !
233         waitForAssert(() -> {
234             assertThat(feedThing.getStatus(), anyOf(is(ThingStatus.ONLINE), is(ThingStatus.OFFLINE)));
235         }, 60000, DFL_SLEEP_TIME);
236         initializeItem(channelUID);
237     }
238
239     private void initializeItem(ChannelUID channelUID) {
240         // Create new item
241         ItemRegistry itemRegistry = getService(ItemRegistry.class);
242         assertThat(itemRegistry, is(notNullValue()));
243
244         StringItem newItem = new StringItem(ITEM_NAME);
245
246         // Add item state change listener
247         StateChangeListener updateListener = new StateChangeListener() {
248             @Override
249             public void stateChanged(Item item, State oldState, State newState) {
250             }
251
252             @Override
253             public void stateUpdated(Item item, State state) {
254                 currentItemState = (StringType) state;
255             }
256         };
257
258         newItem.addStateChangeListener(updateListener);
259         itemRegistry.add(newItem);
260
261         // Add item channel link
262         ManagedItemChannelLinkProvider itemChannelLinkProvider = getService(ManagedItemChannelLinkProvider.class);
263         assertThat(itemChannelLinkProvider, is(notNullValue()));
264         itemChannelLinkProvider.add(new ItemChannelLink(ITEM_NAME, channelUID));
265     }
266
267     private void testIfItemStateIsUpdated(boolean commandReceived, boolean contentChanged)
268             throws IOException, InterruptedException {
269         initializeDefaultFeedHandler();
270
271         waitForAssert(() -> {
272             assertThat("Feed Thing can not be initialized", feedThing.getStatus(), is(equalTo(ThingStatus.ONLINE)));
273             assertThat("Item's state is not updated on initialize", currentItemState, is(notNullValue()));
274         });
275
276         assertThat(currentItemState, is(instanceOf(StringType.class)));
277         StringType firstItemState = currentItemState;
278
279         if (contentChanged) {
280             // The content on the mocked server should be changed
281             servlet.setFeedContent(MOCK_CONTENT_CHANGED);
282         }
283
284         if (commandReceived) {
285             // Before this time has expired, the refresh command will no trigger a request to the server
286             sleep(FeedBindingConstants.MINIMUM_REFRESH_TIME);
287
288             feedHandler.handleCommand(channelUID, RefreshType.REFRESH);
289         } else {
290             // The auto refresh task will handle the update after the default wait time
291             sleep(DEFAULT_TEST_AUTOREFRESH_TIME * 60 * 1000);
292         }
293
294         waitForAssert(() -> {
295             assertThat("Error occurred while trying to connect to server. Content is not downloaded!",
296                     feedThing.getStatus(), is(equalTo(ThingStatus.ONLINE)));
297         });
298
299         waitForAssert(() -> {
300             if (contentChanged) {
301                 assertThat("Content is not updated!", currentItemState, not(equalTo(firstItemState)));
302             } else {
303                 assertThat(currentItemState, is(equalTo(firstItemState)));
304             }
305         });
306     }
307
308     @Test
309     public void assertThatInvalidConfigurationFallsBackToDefaultValues() {
310         String mockServletURL = generateURLString(MOCK_SERVLET_PROTOCOL, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
311                 MOCK_SERVLET_PATH);
312         BigDecimal defaultTestRefreshInterval = new BigDecimal(-10);
313         initializeFeedHandler(mockServletURL, defaultTestRefreshInterval);
314     }
315
316     @Test
317     public void assertThatItemsStateIsNotUpdatedOnAutoRefreshIfContentIsNotChanged()
318             throws IOException, InterruptedException {
319         boolean commandReceived = false;
320         boolean contentChanged = false;
321         testIfItemStateIsUpdated(commandReceived, contentChanged);
322     }
323
324     @Test
325     public void assertThatItemsStateIsUpdatedOnAutoRefreshIfContentChanged() throws IOException, InterruptedException {
326         boolean commandReceived = false;
327         boolean contentChanged = true;
328         testIfItemStateIsUpdated(commandReceived, contentChanged);
329     }
330
331     @Test
332     public void assertThatThingsStatusIsUpdatedWhenHTTP500ErrorCodeIsReceived() throws InterruptedException {
333         testIfThingStatusIsUpdated(HttpStatus.INTERNAL_SERVER_ERROR_500);
334     }
335
336     @Test
337     public void assertThatThingsStatusIsUpdatedWhenHTTP401ErrorCodeIsReceived() throws InterruptedException {
338         testIfThingStatusIsUpdated(HttpStatus.UNAUTHORIZED_401);
339     }
340
341     @Test
342     public void assertThatThingsStatusIsUpdatedWhenHTTP403ErrorCodeIsReceived() throws InterruptedException {
343         testIfThingStatusIsUpdated(HttpStatus.FORBIDDEN_403);
344     }
345
346     @Test
347     public void assertThatThingsStatusIsUpdatedWhenHTTP404ErrorCodeIsReceived() throws InterruptedException {
348         testIfThingStatusIsUpdated(HttpStatus.NOT_FOUND_404);
349     }
350
351     private void testIfThingStatusIsUpdated(Integer serverStatus) throws InterruptedException {
352         initializeDefaultFeedHandler();
353
354         servlet.httpStatus = serverStatus;
355
356         // Before this time has expired, the refresh command will no trigger a request to the server
357         sleep(FeedBindingConstants.MINIMUM_REFRESH_TIME);
358
359         // Invalid channel UID is used for the test, because otherwise
360         feedHandler.handleCommand(channelUID, RefreshType.REFRESH);
361
362         waitForAssert(() -> {
363             assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
364         });
365
366         servlet.httpStatus = HttpStatus.OK_200;
367
368         // Before this time has expired, the refresh command will no trigger a request to the server
369         sleep(FeedBindingConstants.MINIMUM_REFRESH_TIME);
370
371         feedHandler.handleCommand(channelUID, RefreshType.REFRESH);
372
373         waitForAssert(() -> {
374             assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.ONLINE)));
375         });
376     }
377
378     @Test
379     public void createThingWithInvalidUrlProtocol() {
380         String invalidProtocol = "gdfs";
381         String invalidURL = generateURLString(invalidProtocol, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
382                 MOCK_SERVLET_PATH);
383
384         initializeFeedHandler(invalidURL);
385         waitForAssert(() -> {
386             assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
387             assertThat(feedThing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
388         });
389     }
390
391     @Test
392     public void createThingWithInvalidUrlHostname() {
393         String invalidHostname = "invalidhost";
394         String invalidURL = generateURLString(MOCK_SERVLET_PROTOCOL, invalidHostname, MOCK_SERVLET_PORT,
395                 MOCK_SERVLET_PATH);
396
397         initializeFeedHandler(invalidURL);
398         waitForAssert(() -> {
399             assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
400             assertThat(feedThing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.COMMUNICATION_ERROR)));
401         }, 30000, DFL_SLEEP_TIME);
402     }
403
404     @Test
405     public void createThingWithInvalidUrlPath() {
406         String invalidPath = "/invalid/path";
407         String invalidURL = generateURLString(MOCK_SERVLET_PROTOCOL, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
408                 invalidPath);
409
410         initializeFeedHandler(invalidURL);
411         waitForAssert(() -> {
412             assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
413             assertThat(feedThing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.COMMUNICATION_ERROR)));
414         });
415     }
416 }