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