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;
109 * This class is used as a mock for HTTP web server, serving XML feed content.
111 class FeedServiceMock extends HttpServlet {
112 private static final long serialVersionUID = -7810045624309790473L;
117 public FeedServiceMock(String feedContentFile) {
120 setFeedContent(feedContentFile);
121 } catch (IOException e) {
122 throw new IllegalArgumentException("Error loading feed content from: " + feedContentFile);
124 // By default the servlet returns HTTP Status code 200 OK
125 this.httpStatus = HttpStatus.OK_200;
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);
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);
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(ThingStatus.ONLINE), is(ThingStatus.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(ThingStatus.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(ThingStatus.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 public void assertThatItemsStateIsNotUpdatedOnAutoRefreshIfContentIsNotChanged()
319 throws IOException, InterruptedException {
320 boolean commandReceived = false;
321 boolean contentChanged = false;
322 testIfItemStateIsUpdated(commandReceived, contentChanged);
326 public void assertThatItemsStateIsUpdatedOnAutoRefreshIfContentChanged() throws IOException, InterruptedException {
327 boolean commandReceived = false;
328 boolean contentChanged = true;
329 testIfItemStateIsUpdated(commandReceived, contentChanged);
333 public void assertThatThingsStatusIsUpdatedWhenHTTP500ErrorCodeIsReceived() throws InterruptedException {
334 testIfThingStatusIsUpdated(HttpStatus.INTERNAL_SERVER_ERROR_500);
338 public void assertThatThingsStatusIsUpdatedWhenHTTP401ErrorCodeIsReceived() throws InterruptedException {
339 testIfThingStatusIsUpdated(HttpStatus.UNAUTHORIZED_401);
343 public void assertThatThingsStatusIsUpdatedWhenHTTP403ErrorCodeIsReceived() throws InterruptedException {
344 testIfThingStatusIsUpdated(HttpStatus.FORBIDDEN_403);
348 public void assertThatThingsStatusIsUpdatedWhenHTTP404ErrorCodeIsReceived() throws InterruptedException {
349 testIfThingStatusIsUpdated(HttpStatus.NOT_FOUND_404);
352 private void testIfThingStatusIsUpdated(Integer serverStatus) throws InterruptedException {
353 initializeDefaultFeedHandler();
355 servlet.httpStatus = serverStatus;
357 // Before this time has expired, the refresh command will no trigger a request to the server
358 sleep(FeedBindingConstants.MINIMUM_REFRESH_TIME);
360 // Invalid channel UID is used for the test, because otherwise
361 feedHandler.handleCommand(channelUID, RefreshType.REFRESH);
363 waitForAssert(() -> {
364 assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
367 servlet.httpStatus = HttpStatus.OK_200;
369 // Before this time has expired, the refresh command will no trigger a request to the server
370 sleep(FeedBindingConstants.MINIMUM_REFRESH_TIME);
372 feedHandler.handleCommand(channelUID, RefreshType.REFRESH);
374 waitForAssert(() -> {
375 assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.ONLINE)));
380 public void createThingWithInvalidUrlProtocol() {
381 String invalidProtocol = "gdfs";
382 String invalidURL = generateURLString(invalidProtocol, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
385 initializeFeedHandler(invalidURL);
386 waitForAssert(() -> {
387 assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
388 assertThat(feedThing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
393 public void createThingWithInvalidUrlHostname() {
394 String invalidHostname = "invalidhost";
395 String invalidURL = generateURLString(MOCK_SERVLET_PROTOCOL, invalidHostname, MOCK_SERVLET_PORT,
398 initializeFeedHandler(invalidURL);
399 waitForAssert(() -> {
400 assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
401 assertThat(feedThing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.COMMUNICATION_ERROR)));
402 }, 30000, DFL_SLEEP_TIME);
406 public void createThingWithInvalidUrlPath() {
407 String invalidPath = "/invalid/path";
408 String invalidURL = generateURLString(MOCK_SERVLET_PROTOCOL, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
411 initializeFeedHandler(invalidURL);
412 waitForAssert(() -> {
413 assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
414 assertThat(feedThing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.COMMUNICATION_ERROR)));