]> git.basschouten.com Git - openhab-addons.git/blob
87b7283b43b6b1a8750c20dca45aa5cbb1eaa45a
[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 import static org.openhab.core.thing.ThingStatus.*;
19
20 import java.io.IOException;
21 import java.math.BigDecimal;
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.apache.commons.io.IOUtils;
29 import org.eclipse.jetty.http.HttpStatus;
30 import org.junit.jupiter.api.AfterEach;
31 import org.junit.jupiter.api.BeforeEach;
32 import org.junit.jupiter.api.Test;
33 import org.openhab.binding.feed.internal.FeedBindingConstants;
34 import org.openhab.binding.feed.internal.handler.FeedHandler;
35 import org.openhab.core.config.core.Configuration;
36 import org.openhab.core.items.Item;
37 import org.openhab.core.items.ItemRegistry;
38 import org.openhab.core.items.StateChangeListener;
39 import org.openhab.core.library.items.StringItem;
40 import org.openhab.core.library.types.StringType;
41 import org.openhab.core.test.java.JavaOSGiTest;
42 import org.openhab.core.test.storage.VolatileStorageService;
43 import org.openhab.core.thing.Channel;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.ManagedThingProvider;
46 import org.openhab.core.thing.Thing;
47 import org.openhab.core.thing.ThingProvider;
48 import org.openhab.core.thing.ThingRegistry;
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 = null;
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
108     /**
109      * This class is used as a mock for HTTP web server, serving XML feed content.
110      */
111     class FeedServiceMock extends HttpServlet {
112         private static final long serialVersionUID = -7810045624309790473L;
113
114         String feedContent;
115         int httpStatus;
116
117         public FeedServiceMock(String feedContentFile) {
118             super();
119             try {
120                 setFeedContent(feedContentFile);
121             } catch (IOException e) {
122                 throw new IllegalArgumentException("Error loading feed content from: " + feedContentFile);
123             }
124             // By default the servlet returns HTTP Status code 200 OK
125             this.httpStatus = HttpStatus.OK_200;
126         }
127
128         @Override
129         protected void doGet(HttpServletRequest request, HttpServletResponse response)
130                 throws ServletException, IOException {
131             response.getOutputStream().println(feedContent);
132             // Recommended RSS MIME type - http://www.rssboard.org/rss-mime-type-application.txt
133             // Atom MIME type is - application/atom+xml
134             // Other MIME types - text/plan, text/xml, text/html are tested and accepted as well
135             response.setContentType("application/rss+xml");
136             response.setStatus(httpStatus);
137         }
138
139         public void setFeedContent(String feedContentFile) throws IOException {
140             String path = "input/" + feedContentFile;
141             feedContent = IOUtils.toString(this.getClass().getClassLoader().getResourceAsStream(path));
142         }
143     }
144
145     @BeforeEach
146     public void setUp() {
147         volatileStorageService = new VolatileStorageService();
148         registerService(volatileStorageService);
149
150         managedThingProvider = getService(ThingProvider.class, ManagedThingProvider.class);
151         assertThat(managedThingProvider, is(notNullValue()));
152
153         thingRegistry = getService(ThingRegistry.class);
154         assertThat(thingRegistry, is(notNullValue()));
155
156         registerFeedTestServlet();
157     }
158
159     @AfterEach
160     public void tearDown() {
161         currentItemState = null;
162         if (feedThing != null) {
163             // Remove the feed thing. The handler will be also disposed automatically
164             Thing removedThing = thingRegistry.forceRemove(feedThing.getUID());
165             assertThat("The feed thing cannot be deleted", removedThing, is(notNullValue()));
166         }
167
168         unregisterFeedTestServlet();
169
170         // Wait for FeedHandler to be unregistered
171         waitForAssert(() -> {
172             feedHandler = (FeedHandler) feedThing.getHandler();
173             assertThat(feedHandler, is(nullValue()));
174         });
175     }
176
177     private void registerFeedTestServlet() {
178         HttpService httpService = getService(HttpService.class);
179         assertThat(httpService, is(notNullValue()));
180         servlet = new FeedServiceMock(DEFAULT_MOCK_CONTENT);
181         try {
182             httpService.registerServlet(MOCK_SERVLET_PATH, servlet, null, null);
183         } catch (ServletException | NamespaceException e) {
184             throw new IllegalStateException("Failed to register feed test servlet", e);
185         }
186     }
187
188     private void unregisterFeedTestServlet() {
189         HttpService httpService = getService(HttpService.class);
190         assertThat(httpService, 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(ONLINE), is(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(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(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(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(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(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(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(OFFLINE)));
413             assertThat(feedThing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.COMMUNICATION_ERROR)));
414         });
415     }
416 }