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