2 * Copyright (c) 2010-2023 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 if (feedThing != null) {
173 // Wait for FeedHandler to be unregistered
174 waitForAssert(() -> {
175 feedHandler = (FeedHandler) feedThing.getHandler();
176 assertThat(feedHandler, is(nullValue()));
181 private synchronized void registerFeedTestServlet() {
182 waitForAssert(() -> assertThat(httpService = getService(HttpService.class), is(notNullValue())));
183 servlet = new FeedServiceMock(DEFAULT_MOCK_CONTENT);
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);
191 private synchronized void unregisterFeedTestServlet() {
192 waitForAssert(() -> assertThat(httpService = getService(HttpService.class), is(notNullValue())));
194 httpService.unregister(MOCK_SERVLET_PATH);
195 } catch (IllegalArgumentException ignore) {
200 private String generateURLString(String protocol, String hostname, int port, String path) {
201 return protocol + "://" + hostname + ":" + port + path;
204 private void initializeDefaultFeedHandler() {
205 String mockServletURL = generateURLString(MOCK_SERVLET_PROTOCOL, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
207 // One minute update time is used for the tests
208 BigDecimal defaultTestRefreshInterval = new BigDecimal(DEFAULT_TEST_AUTOREFRESH_TIME);
209 initializeFeedHandler(mockServletURL, defaultTestRefreshInterval);
212 private void initializeFeedHandler(String url) {
213 initializeFeedHandler(url, null);
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);
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();
229 managedThingProvider.add(feedThing);
231 // Wait for FeedHandler to be registered
232 waitForAssert(() -> {
233 feedHandler = (FeedHandler) feedThing.getHandler();
234 assertThat("FeedHandler is not registered", feedHandler, is(notNullValue()));
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);
244 private void initializeItem(ChannelUID channelUID) {
246 ItemRegistry itemRegistry = getService(ItemRegistry.class);
247 assertThat(itemRegistry, is(notNullValue()));
249 StringItem newItem = new StringItem(ITEM_NAME);
251 // Add item state change listener
252 StateChangeListener updateListener = new StateChangeListener() {
254 public void stateChanged(Item item, State oldState, State newState) {
258 public void stateUpdated(Item item, State state) {
259 currentItemState = (StringType) state;
263 newItem.addStateChangeListener(updateListener);
264 itemRegistry.add(newItem);
266 // Add item channel link
267 ManagedItemChannelLinkProvider itemChannelLinkProvider = getService(ManagedItemChannelLinkProvider.class);
268 assertThat(itemChannelLinkProvider, is(notNullValue()));
269 itemChannelLinkProvider.add(new ItemChannelLink(ITEM_NAME, channelUID));
272 private void testIfItemStateIsUpdated(boolean commandReceived, boolean contentChanged)
273 throws IOException, InterruptedException {
274 initializeDefaultFeedHandler();
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()));
281 assertThat(currentItemState, is(instanceOf(StringType.class)));
282 StringType firstItemState = currentItemState;
284 if (contentChanged) {
285 // The content on the mocked server should be changed
286 servlet.setFeedContent(MOCK_CONTENT_CHANGED);
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);
293 feedHandler.handleCommand(channelUID, RefreshType.REFRESH);
295 // The auto refresh task will handle the update after the default wait time
296 sleep(DEFAULT_TEST_AUTOREFRESH_TIME * 60 * 1000);
299 waitForAssert(() -> {
300 assertThat("Error occurred while trying to connect to server. Content is not downloaded!",
301 feedThing.getStatus(), is(equalTo(ThingStatus.ONLINE)));
304 waitForAssert(() -> {
305 if (contentChanged) {
306 assertThat("Content is not updated!", currentItemState, not(equalTo(firstItemState)));
308 assertThat(currentItemState, is(equalTo(firstItemState)));
314 public void assertThatInvalidConfigurationFallsBackToDefaultValues() {
315 String mockServletURL = generateURLString(MOCK_SERVLET_PROTOCOL, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
317 BigDecimal defaultTestRefreshInterval = new BigDecimal(-10);
318 initializeFeedHandler(mockServletURL, defaultTestRefreshInterval);
322 public void assertThatItemsStateIsNotUpdatedOnAutoRefreshIfContentIsNotChanged()
323 throws IOException, InterruptedException {
324 boolean commandReceived = false;
325 boolean contentChanged = false;
326 testIfItemStateIsUpdated(commandReceived, contentChanged);
330 public void assertThatItemsStateIsUpdatedOnAutoRefreshIfContentChanged() throws IOException, InterruptedException {
331 boolean commandReceived = false;
332 boolean contentChanged = true;
333 testIfItemStateIsUpdated(commandReceived, contentChanged);
337 public void assertThatThingsStatusIsUpdatedWhenHTTP500ErrorCodeIsReceived() throws InterruptedException {
338 testIfThingStatusIsUpdated(HttpStatus.INTERNAL_SERVER_ERROR_500);
342 public void assertThatThingsStatusIsUpdatedWhenHTTP401ErrorCodeIsReceived() throws InterruptedException {
343 testIfThingStatusIsUpdated(HttpStatus.UNAUTHORIZED_401);
347 public void assertThatThingsStatusIsUpdatedWhenHTTP403ErrorCodeIsReceived() throws InterruptedException {
348 testIfThingStatusIsUpdated(HttpStatus.FORBIDDEN_403);
352 public void assertThatThingsStatusIsUpdatedWhenHTTP404ErrorCodeIsReceived() throws InterruptedException {
353 testIfThingStatusIsUpdated(HttpStatus.NOT_FOUND_404);
356 private void testIfThingStatusIsUpdated(Integer serverStatus) throws InterruptedException {
357 initializeDefaultFeedHandler();
359 servlet.httpStatus = serverStatus;
361 // Before this time has expired, the refresh command will no trigger a request to the server
362 sleep(FeedBindingConstants.MINIMUM_REFRESH_TIME);
364 // Invalid channel UID is used for the test, because otherwise
365 feedHandler.handleCommand(channelUID, RefreshType.REFRESH);
367 waitForAssert(() -> {
368 assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
371 servlet.httpStatus = HttpStatus.OK_200;
373 // Before this time has expired, the refresh command will no trigger a request to the server
374 sleep(FeedBindingConstants.MINIMUM_REFRESH_TIME);
376 feedHandler.handleCommand(channelUID, RefreshType.REFRESH);
378 waitForAssert(() -> {
379 assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.ONLINE)));
384 public void createThingWithInvalidUrlProtocol() {
385 String invalidProtocol = "gdfs";
386 String invalidURL = generateURLString(invalidProtocol, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
389 initializeFeedHandler(invalidURL);
390 waitForAssert(() -> {
391 assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
392 assertThat(feedThing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
397 public void createThingWithInvalidUrlHostname() {
398 String invalidHostname = "invalidhost";
399 String invalidURL = generateURLString(MOCK_SERVLET_PROTOCOL, invalidHostname, MOCK_SERVLET_PORT,
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);
410 public void createThingWithInvalidUrlPath() {
411 String invalidPath = "/invalid/path";
412 String invalidURL = generateURLString(MOCK_SERVLET_PROTOCOL, MOCK_SERVLET_HOSTNAME, MOCK_SERVLET_PORT,
415 initializeFeedHandler(invalidURL);
416 waitForAssert(() -> {
417 assertThat(feedThing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
418 assertThat(feedThing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.COMMUNICATION_ERROR)));