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())));
191 httpService.unregister(MOCK_SERVLET_PATH);
195 private String generateURLString(String protocol, String hostname, int port, String path) {
196 return protocol + "://" + hostname + ":" + port + path;
199 private void initializeDefaultFeedHandler() {
200 String mockServletURL = generateURLString(MOCK_SERVLET_PROTOCOL, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
202 // One minute update time is used for the tests
203 BigDecimal defaultTestRefreshInterval = new BigDecimal(DEFAULT_TEST_AUTOREFRESH_TIME);
204 initializeFeedHandler(mockServletURL, defaultTestRefreshInterval);
207 private void initializeFeedHandler(String url) {
208 initializeFeedHandler(url, null);
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);
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();
224 managedThingProvider.add(feedThing);
226 // Wait for FeedHandler to be registered
227 waitForAssert(() -> {
228 feedHandler = (FeedHandler) feedThing.getHandler();
229 assertThat("FeedHandler is not registered", feedHandler, is(notNullValue()));
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(ThingStatus.ONLINE), is(ThingStatus.OFFLINE)));
235 }, 60000, DFL_SLEEP_TIME);
236 initializeItem(channelUID);
239 private void initializeItem(ChannelUID channelUID) {
241 ItemRegistry itemRegistry = getService(ItemRegistry.class);
242 assertThat(itemRegistry, is(notNullValue()));
244 StringItem newItem = new StringItem(ITEM_NAME);
246 // Add item state change listener
247 StateChangeListener updateListener = new StateChangeListener() {
249 public void stateChanged(Item item, State oldState, State newState) {
253 public void stateUpdated(Item item, State state) {
254 currentItemState = (StringType) state;
258 newItem.addStateChangeListener(updateListener);
259 itemRegistry.add(newItem);
261 // Add item channel link
262 ManagedItemChannelLinkProvider itemChannelLinkProvider = getService(ManagedItemChannelLinkProvider.class);
263 assertThat(itemChannelLinkProvider, is(notNullValue()));
264 itemChannelLinkProvider.add(new ItemChannelLink(ITEM_NAME, channelUID));
267 private void testIfItemStateIsUpdated(boolean commandReceived, boolean contentChanged)
268 throws IOException, InterruptedException {
269 initializeDefaultFeedHandler();
271 waitForAssert(() -> {
272 assertThat("Feed Thing can not be initialized", feedThing.getStatus(), is(equalTo(ThingStatus.ONLINE)));
273 assertThat("Item's state is not updated on initialize", currentItemState, is(notNullValue()));
276 assertThat(currentItemState, is(instanceOf(StringType.class)));
277 StringType firstItemState = currentItemState;
279 if (contentChanged) {
280 // The content on the mocked server should be changed
281 servlet.setFeedContent(MOCK_CONTENT_CHANGED);
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);
288 feedHandler.handleCommand(channelUID, RefreshType.REFRESH);
290 // The auto refresh task will handle the update after the default wait time
291 sleep(DEFAULT_TEST_AUTOREFRESH_TIME * 60 * 1000);
294 waitForAssert(() -> {
295 assertThat("Error occurred while trying to connect to server. Content is not downloaded!",
296 feedThing.getStatus(), is(equalTo(ThingStatus.ONLINE)));
299 waitForAssert(() -> {
300 if (contentChanged) {
301 assertThat("Content is not updated!", currentItemState, not(equalTo(firstItemState)));
303 assertThat(currentItemState, is(equalTo(firstItemState)));
309 public void assertThatInvalidConfigurationFallsBackToDefaultValues() {
310 String mockServletURL = generateURLString(MOCK_SERVLET_PROTOCOL, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
312 BigDecimal defaultTestRefreshInterval = new BigDecimal(-10);
313 initializeFeedHandler(mockServletURL, defaultTestRefreshInterval);
317 public void assertThatItemsStateIsNotUpdatedOnAutoRefreshIfContentIsNotChanged()
318 throws IOException, InterruptedException {
319 boolean commandReceived = false;
320 boolean contentChanged = false;
321 testIfItemStateIsUpdated(commandReceived, contentChanged);
325 public void assertThatItemsStateIsUpdatedOnAutoRefreshIfContentChanged() throws IOException, InterruptedException {
326 boolean commandReceived = false;
327 boolean contentChanged = true;
328 testIfItemStateIsUpdated(commandReceived, contentChanged);
332 public void assertThatThingsStatusIsUpdatedWhenHTTP500ErrorCodeIsReceived() throws InterruptedException {
333 testIfThingStatusIsUpdated(HttpStatus.INTERNAL_SERVER_ERROR_500);
337 public void assertThatThingsStatusIsUpdatedWhenHTTP401ErrorCodeIsReceived() throws InterruptedException {
338 testIfThingStatusIsUpdated(HttpStatus.UNAUTHORIZED_401);
342 public void assertThatThingsStatusIsUpdatedWhenHTTP403ErrorCodeIsReceived() throws InterruptedException {
343 testIfThingStatusIsUpdated(HttpStatus.FORBIDDEN_403);
347 public void assertThatThingsStatusIsUpdatedWhenHTTP404ErrorCodeIsReceived() throws InterruptedException {
348 testIfThingStatusIsUpdated(HttpStatus.NOT_FOUND_404);
351 private void testIfThingStatusIsUpdated(Integer serverStatus) throws InterruptedException {
352 initializeDefaultFeedHandler();
354 servlet.httpStatus = serverStatus;
356 // Before this time has expired, the refresh command will no trigger a request to the server
357 sleep(FeedBindingConstants.MINIMUM_REFRESH_TIME);
359 // Invalid channel UID is used for the test, because otherwise
360 feedHandler.handleCommand(channelUID, RefreshType.REFRESH);
362 waitForAssert(() -> {
363 assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
366 servlet.httpStatus = HttpStatus.OK_200;
368 // Before this time has expired, the refresh command will no trigger a request to the server
369 sleep(FeedBindingConstants.MINIMUM_REFRESH_TIME);
371 feedHandler.handleCommand(channelUID, RefreshType.REFRESH);
373 waitForAssert(() -> {
374 assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.ONLINE)));
379 public void createThingWithInvalidUrlProtocol() {
380 String invalidProtocol = "gdfs";
381 String invalidURL = generateURLString(invalidProtocol, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
384 initializeFeedHandler(invalidURL);
385 waitForAssert(() -> {
386 assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
387 assertThat(feedThing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
392 public void createThingWithInvalidUrlHostname() {
393 String invalidHostname = "invalidhost";
394 String invalidURL = generateURLString(MOCK_SERVLET_PROTOCOL, invalidHostname, MOCK_SERVLET_PORT,
397 initializeFeedHandler(invalidURL);
398 waitForAssert(() -> {
399 assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
400 assertThat(feedThing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.COMMUNICATION_ERROR)));
401 }, 30000, DFL_SLEEP_TIME);
405 public void createThingWithInvalidUrlPath() {
406 String invalidPath = "/invalid/path";
407 String invalidURL = generateURLString(MOCK_SERVLET_PROTOCOL, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
410 initializeFeedHandler(invalidURL);
411 waitForAssert(() -> {
412 assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
413 assertThat(feedThing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.COMMUNICATION_ERROR)));