2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.feed.test;
15 import static java.lang.Thread.sleep;
16 import static org.hamcrest.CoreMatchers.*;
17 import static org.hamcrest.MatcherAssert.assertThat;
19 import java.io.IOException;
20 import java.math.BigDecimal;
21 import java.nio.charset.StandardCharsets;
23 import javax.servlet.ServletException;
24 import javax.servlet.http.HttpServlet;
25 import javax.servlet.http.HttpServletRequest;
26 import javax.servlet.http.HttpServletResponse;
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;
61 * Tests for {@link FeedHandler}
63 * @author Svilen Valkanov - Initial contribution
64 * @author Wouter Born - Migrate Groovy to Java tests
66 public class FeedHandlerTest extends JavaOSGiTest {
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";
74 // Files used for the test as input. They are located in /src/test/resources directory
76 * The default mock content in the test is RSS 2.0 format, as this is the most popular format
78 private static final String DEFAULT_MOCK_CONTENT = "rss_2.0.xml";
81 * One new entry is added to {@link #DEFAULT_MOCK_CONTENT}
83 private static final String MOCK_CONTENT_CHANGED = "rss_2.0_changed.xml";
85 private static final String ITEM_NAME = "testItem";
86 private static final String THING_NAME = "testFeedThing";
89 * Default auto refresh interval for the test is 1 Minute.
91 private static final int DEFAULT_TEST_AUTOREFRESH_TIME = 1;
94 * It is updated from mocked {@link StateChangeListener#stateUpdated() }
96 private StringType currentItemState;
98 // Required services for the test
99 private ManagedThingProvider managedThingProvider;
100 private VolatileStorageService volatileStorageService;
101 private ThingRegistry thingRegistry;
103 private FeedServiceMock servlet;
104 private Thing feedThing;
105 private FeedHandler feedHandler;
106 private ChannelUID channelUID;
107 private HttpService httpService;
110 * This class is used as a mock for HTTP web server, serving XML feed content.
112 class FeedServiceMock extends HttpServlet {
113 private static final long serialVersionUID = -7810045624309790473L;
118 public FeedServiceMock(String feedContentFile) {
121 setFeedContent(feedContentFile);
122 } catch (IOException e) {
123 throw new IllegalArgumentException("Error loading feed content from: " + feedContentFile);
125 // By default the servlet returns HTTP Status code 200 OK
126 this.httpStatus = HttpStatus.OK_200;
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);
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);
148 public void setUp() {
149 volatileStorageService = new VolatileStorageService();
150 registerService(volatileStorageService);
152 managedThingProvider = getService(ThingProvider.class, ManagedThingProvider.class);
153 assertThat(managedThingProvider, is(notNullValue()));
155 thingRegistry = getService(ThingRegistry.class);
156 assertThat(thingRegistry, is(notNullValue()));
158 registerFeedTestServlet();
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()));
170 unregisterFeedTestServlet();
172 // Wait for FeedHandler to be unregistered
173 waitForAssert(() -> {
174 feedHandler = (FeedHandler) feedThing.getHandler();
175 assertThat(feedHandler, is(nullValue()));
179 private synchronized void registerFeedTestServlet() {
180 waitForAssert(() -> assertThat(httpService = getService(HttpService.class), is(notNullValue())));
181 servlet = new FeedServiceMock(DEFAULT_MOCK_CONTENT);
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);
189 private synchronized void unregisterFeedTestServlet() {
190 waitForAssert(() -> assertThat(httpService = getService(HttpService.class), is(notNullValue())));
192 httpService.unregister(MOCK_SERVLET_PATH);
193 } catch (IllegalArgumentException ignore) {
198 private String generateURLString(String protocol, String hostname, int port, String path) {
199 return protocol + "://" + hostname + ":" + port + path;
202 private void initializeDefaultFeedHandler() {
203 String mockServletURL = generateURLString(MOCK_SERVLET_PROTOCOL, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
205 // One minute update time is used for the tests
206 BigDecimal defaultTestRefreshInterval = new BigDecimal(DEFAULT_TEST_AUTOREFRESH_TIME);
207 initializeFeedHandler(mockServletURL, defaultTestRefreshInterval);
210 private void initializeFeedHandler(String url) {
211 initializeFeedHandler(url, null);
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);
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();
227 managedThingProvider.add(feedThing);
229 // Wait for FeedHandler to be registered
230 waitForAssert(() -> {
231 feedHandler = (FeedHandler) feedThing.getHandler();
232 assertThat("FeedHandler is not registered", feedHandler, is(notNullValue()));
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);
242 private void initializeItem(ChannelUID channelUID) {
244 ItemRegistry itemRegistry = getService(ItemRegistry.class);
245 assertThat(itemRegistry, is(notNullValue()));
247 StringItem newItem = new StringItem(ITEM_NAME);
249 // Add item state change listener
250 StateChangeListener updateListener = new StateChangeListener() {
252 public void stateChanged(Item item, State oldState, State newState) {
256 public void stateUpdated(Item item, State state) {
257 currentItemState = (StringType) state;
261 newItem.addStateChangeListener(updateListener);
262 itemRegistry.add(newItem);
264 // Add item channel link
265 ManagedItemChannelLinkProvider itemChannelLinkProvider = getService(ManagedItemChannelLinkProvider.class);
266 assertThat(itemChannelLinkProvider, is(notNullValue()));
267 itemChannelLinkProvider.add(new ItemChannelLink(ITEM_NAME, channelUID));
270 private void testIfItemStateIsUpdated(boolean commandReceived, boolean contentChanged)
271 throws IOException, InterruptedException {
272 initializeDefaultFeedHandler();
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()));
279 assertThat(currentItemState, is(instanceOf(StringType.class)));
280 StringType firstItemState = currentItemState;
282 if (contentChanged) {
283 // The content on the mocked server should be changed
284 servlet.setFeedContent(MOCK_CONTENT_CHANGED);
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);
291 feedHandler.handleCommand(channelUID, RefreshType.REFRESH);
293 // The auto refresh task will handle the update after the default wait time
294 sleep(DEFAULT_TEST_AUTOREFRESH_TIME * 60 * 1000);
297 waitForAssert(() -> {
298 assertThat("Error occurred while trying to connect to server. Content is not downloaded!",
299 feedThing.getStatus(), is(equalTo(ThingStatus.ONLINE)));
302 waitForAssert(() -> {
303 if (contentChanged) {
304 assertThat("Content is not updated!", currentItemState, not(equalTo(firstItemState)));
306 assertThat(currentItemState, is(equalTo(firstItemState)));
312 public void assertThatInvalidConfigurationFallsBackToDefaultValues() {
313 String mockServletURL = generateURLString(MOCK_SERVLET_PROTOCOL, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
315 BigDecimal defaultTestRefreshInterval = new BigDecimal(-10);
316 initializeFeedHandler(mockServletURL, defaultTestRefreshInterval);
320 public void assertThatItemsStateIsNotUpdatedOnAutoRefreshIfContentIsNotChanged()
321 throws IOException, InterruptedException {
322 boolean commandReceived = false;
323 boolean contentChanged = false;
324 testIfItemStateIsUpdated(commandReceived, contentChanged);
328 public void assertThatItemsStateIsUpdatedOnAutoRefreshIfContentChanged() throws IOException, InterruptedException {
329 boolean commandReceived = false;
330 boolean contentChanged = true;
331 testIfItemStateIsUpdated(commandReceived, contentChanged);
335 public void assertThatThingsStatusIsUpdatedWhenHTTP500ErrorCodeIsReceived() throws InterruptedException {
336 testIfThingStatusIsUpdated(HttpStatus.INTERNAL_SERVER_ERROR_500);
340 public void assertThatThingsStatusIsUpdatedWhenHTTP401ErrorCodeIsReceived() throws InterruptedException {
341 testIfThingStatusIsUpdated(HttpStatus.UNAUTHORIZED_401);
345 public void assertThatThingsStatusIsUpdatedWhenHTTP403ErrorCodeIsReceived() throws InterruptedException {
346 testIfThingStatusIsUpdated(HttpStatus.FORBIDDEN_403);
350 public void assertThatThingsStatusIsUpdatedWhenHTTP404ErrorCodeIsReceived() throws InterruptedException {
351 testIfThingStatusIsUpdated(HttpStatus.NOT_FOUND_404);
354 private void testIfThingStatusIsUpdated(Integer serverStatus) throws InterruptedException {
355 initializeDefaultFeedHandler();
357 servlet.httpStatus = serverStatus;
359 // Before this time has expired, the refresh command will no trigger a request to the server
360 sleep(FeedBindingConstants.MINIMUM_REFRESH_TIME);
362 // Invalid channel UID is used for the test, because otherwise
363 feedHandler.handleCommand(channelUID, RefreshType.REFRESH);
365 waitForAssert(() -> {
366 assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
369 servlet.httpStatus = HttpStatus.OK_200;
371 // Before this time has expired, the refresh command will no trigger a request to the server
372 sleep(FeedBindingConstants.MINIMUM_REFRESH_TIME);
374 feedHandler.handleCommand(channelUID, RefreshType.REFRESH);
376 waitForAssert(() -> {
377 assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.ONLINE)));
382 public void createThingWithInvalidUrlProtocol() {
383 String invalidProtocol = "gdfs";
384 String invalidURL = generateURLString(invalidProtocol, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
387 initializeFeedHandler(invalidURL);
388 waitForAssert(() -> {
389 assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
390 assertThat(feedThing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
395 public void createThingWithInvalidUrlHostname() {
396 String invalidHostname = "invalidhost";
397 String invalidURL = generateURLString(MOCK_SERVLET_PROTOCOL, invalidHostname, MOCK_SERVLET_PORT,
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);
408 public void createThingWithInvalidUrlPath() {
409 String invalidPath = "/invalid/path";
410 String invalidURL = generateURLString(MOCK_SERVLET_PROTOCOL, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
413 initializeFeedHandler(invalidURL);
414 waitForAssert(() -> {
415 assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
416 assertThat(feedThing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.COMMUNICATION_ERROR)));