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.openhab.core.thing.ThingStatus.*;
17 import static org.hamcrest.CoreMatchers.*;
18 import static org.junit.Assert.assertThat;
20 import java.io.IOException;
21 import java.math.BigDecimal;
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.apache.commons.io.IOUtils;
29 import org.eclipse.jetty.http.HttpStatus;
30 import org.openhab.core.config.core.Configuration;
31 import org.openhab.core.items.Item;
32 import org.openhab.core.items.ItemRegistry;
33 import org.openhab.core.items.StateChangeListener;
34 import org.openhab.core.library.items.StringItem;
35 import org.openhab.core.library.types.StringType;
36 import org.openhab.core.thing.Channel;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.ManagedThingProvider;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingProvider;
41 import org.openhab.core.thing.ThingRegistry;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.ThingUID;
44 import org.openhab.core.thing.binding.builder.ChannelBuilder;
45 import org.openhab.core.thing.binding.builder.ThingBuilder;
46 import org.openhab.core.thing.link.ItemChannelLink;
47 import org.openhab.core.thing.link.ManagedItemChannelLinkProvider;
48 import org.openhab.core.types.RefreshType;
49 import org.openhab.core.types.State;
50 import org.openhab.core.test.java.JavaOSGiTest;
51 import org.openhab.core.test.storage.VolatileStorageService;
52 import org.junit.After;
53 import org.junit.Before;
54 import org.junit.Test;
55 import org.junit.experimental.categories.Category;
56 import org.openhab.binding.feed.internal.FeedBindingConstants;
57 import org.openhab.binding.feed.internal.handler.FeedHandler;
58 import org.osgi.service.http.HttpService;
59 import org.osgi.service.http.NamespaceException;
62 * Tests for {@link FeedHandler}
64 * @author Svilen Valkanov - Initial contribution
65 * @author Wouter Born - Migrate Groovy to Java tests
67 public class FeedHandlerTest extends JavaOSGiTest {
69 // Servlet URL configuration
70 private static final String MOCK_SERVLET_PROTOCOL = "http";
71 private static final String MOCK_SERVLET_HOSTNAME = "localhost";
72 private static final int MOCK_SERVLET_PORT = Integer.getInteger("org.osgi.service.http.port", 8080);
73 private static final String MOCK_SERVLET_PATH = "/test/feed";
75 // Files used for the test as input. They are located in /src/test/resources directory
77 * The default mock content in the test is RSS 2.0 format, as this is the most popular format
79 private static final String DEFAULT_MOCK_CONTENT = "rss_2.0.xml";
82 * One new entry is added to {@link #DEFAULT_MOCK_CONTENT}
84 private static final String MOCK_CONTENT_CHANGED = "rss_2.0_changed.xml";
86 private static final String ITEM_NAME = "testItem";
87 private static final String THING_NAME = "testFeedThing";
90 * Default auto refresh interval for the test is 1 Minute.
92 private static final int DEFAULT_TEST_AUTOREFRESH_TIME = 1;
95 * It is updated from mocked {@link StateChangeListener#stateUpdated() }
97 private StringType currentItemState = null;
99 // Required services for the test
100 private ManagedThingProvider managedThingProvider;
101 private VolatileStorageService volatileStorageService;
102 private ThingRegistry thingRegistry;
104 private FeedServiceMock servlet;
105 private Thing feedThing;
106 private FeedHandler feedHandler;
107 private ChannelUID channelUID;
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 = IOUtils.toString(this.getClass().getClassLoader().getResourceAsStream(path));
147 public void setUp() {
148 volatileStorageService = new VolatileStorageService();
149 registerService(volatileStorageService);
151 managedThingProvider = getService(ThingProvider.class, ManagedThingProvider.class);
152 assertThat(managedThingProvider, is(notNullValue()));
154 thingRegistry = getService(ThingRegistry.class);
155 assertThat(thingRegistry, is(notNullValue()));
157 registerFeedTestServlet();
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()));
169 unregisterFeedTestServlet();
171 // Wait for FeedHandler to be unregistered
172 waitForAssert(() -> {
173 feedHandler = (FeedHandler) feedThing.getHandler();
174 assertThat(feedHandler, is(nullValue()));
178 private void registerFeedTestServlet() {
179 HttpService httpService = getService(HttpService.class);
180 assertThat(httpService, 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 void unregisterFeedTestServlet() {
190 HttpService httpService = getService(HttpService.class);
191 assertThat(httpService, is(notNullValue()));
192 httpService.unregister(MOCK_SERVLET_PATH);
196 private String generateURLString(String protocol, String hostname, int port, String path) {
197 return protocol + "://" + hostname + ":" + port + path;
200 private void initializeDefaultFeedHandler() {
201 String mockServletURL = generateURLString(MOCK_SERVLET_PROTOCOL, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
203 // One minute update time is used for the tests
204 BigDecimal defaultTestRefreshInterval = new BigDecimal(DEFAULT_TEST_AUTOREFRESH_TIME);
205 initializeFeedHandler(mockServletURL, defaultTestRefreshInterval);
208 private void initializeFeedHandler(String url) {
209 initializeFeedHandler(url, null);
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);
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();
225 managedThingProvider.add(feedThing);
227 // Wait for FeedHandler to be registered
228 waitForAssert(() -> {
229 feedHandler = (FeedHandler) feedThing.getHandler();
230 assertThat("FeedHandler is not registered", feedHandler, is(notNullValue()));
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);
240 private void initializeItem(ChannelUID channelUID) {
242 ItemRegistry itemRegistry = getService(ItemRegistry.class);
243 assertThat(itemRegistry, is(notNullValue()));
245 StringItem newItem = new StringItem(ITEM_NAME);
247 // Add item state change listener
248 StateChangeListener updateListener = new StateChangeListener() {
250 public void stateChanged(Item item, State oldState, State newState) {
254 public void stateUpdated(Item item, State state) {
255 currentItemState = (StringType) state;
259 newItem.addStateChangeListener(updateListener);
260 itemRegistry.add(newItem);
262 // Add item channel link
263 ManagedItemChannelLinkProvider itemChannelLinkProvider = getService(ManagedItemChannelLinkProvider.class);
264 assertThat(itemChannelLinkProvider, is(notNullValue()));
265 itemChannelLinkProvider.add(new ItemChannelLink(ITEM_NAME, channelUID));
268 private void testIfItemStateIsUpdated(boolean commandReceived, boolean contentChanged)
269 throws IOException, InterruptedException {
270 initializeDefaultFeedHandler();
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()));
277 assertThat(currentItemState, is(instanceOf(StringType.class)));
278 StringType firstItemState = currentItemState;
280 if (contentChanged) {
281 // The content on the mocked server should be changed
282 servlet.setFeedContent(MOCK_CONTENT_CHANGED);
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);
289 feedHandler.handleCommand(channelUID, RefreshType.REFRESH);
291 // The auto refresh task will handle the update after the default wait time
292 sleep(DEFAULT_TEST_AUTOREFRESH_TIME * 60 * 1000);
295 waitForAssert(() -> {
296 assertThat("Error occurred while trying to connect to server. Content is not downloaded!",
297 feedThing.getStatus(), is(equalTo(ONLINE)));
300 waitForAssert(() -> {
301 if (contentChanged) {
302 assertThat("Content is not updated!", currentItemState, not(equalTo(firstItemState)));
304 assertThat(currentItemState, is(equalTo(firstItemState)));
310 public void assertThatInvalidConfigurationFallsBackToDefaultValues() {
311 String mockServletURL = generateURLString(MOCK_SERVLET_PROTOCOL, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
313 BigDecimal defaultTestRefreshInterval = new BigDecimal(-10);
314 initializeFeedHandler(mockServletURL, defaultTestRefreshInterval);
318 @Category(SlowTests.class)
319 public void assertThatItemsStateIsNotUpdatedOnAutoRefreshIfContentIsNotChanged()
320 throws IOException, InterruptedException {
321 boolean commandReceived = false;
322 boolean contentChanged = false;
323 testIfItemStateIsUpdated(commandReceived, contentChanged);
327 @Category(SlowTests.class)
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(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(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(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(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(OFFLINE)));
416 assertThat(feedThing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.COMMUNICATION_ERROR)));