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.upnpcontrol.internal.handler;
15 import static org.eclipse.jdt.annotation.Checks.requireNonNull;
16 import static org.hamcrest.MatcherAssert.assertThat;
17 import static org.hamcrest.Matchers.is;
18 import static org.mockito.ArgumentMatchers.*;
19 import static org.mockito.Mockito.*;
20 import static org.openhab.binding.upnpcontrol.internal.UpnpControlBindingConstants.*;
22 import java.util.Arrays;
23 import java.util.HashMap;
24 import java.util.List;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.concurrent.ConcurrentMap;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.junit.jupiter.api.AfterEach;
32 import org.junit.jupiter.api.BeforeEach;
33 import org.junit.jupiter.api.Test;
34 import org.mockito.ArgumentCaptor;
35 import org.mockito.Mock;
36 import org.openhab.binding.upnpcontrol.internal.config.UpnpControlServerConfiguration;
37 import org.openhab.core.library.types.StringType;
38 import org.openhab.core.thing.Channel;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingUID;
43 import org.openhab.core.thing.binding.builder.ChannelBuilder;
44 import org.openhab.core.types.CommandOption;
45 import org.openhab.core.types.StateOption;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
50 * Unit tests for {@link UpnpServerHandler}.
52 * @author Mark Herwege - Initial contribution
54 @SuppressWarnings({ "null", "unchecked" })
56 public class UpnpServerHandlerTest extends UpnpHandlerTest {
58 private final Logger logger = LoggerFactory.getLogger(UpnpServerHandlerTest.class);
60 private static final String THING_TYPE_UID = "upnpcontrol:upnpserver";
61 private static final String THING_UID = THING_TYPE_UID + ":mockserver";
63 private static final String RESPONSE_HEADER = "<DIDL-Lite xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\" "
64 + "xmlns:dc=\"http://purl.org/dc/elements/1.1/\" "
65 + "xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\">";
66 private static final String RESPONSE_FOOTER = "</DIDL-Lite>";
68 private static final String BASE_CONTAINER = RESPONSE_HEADER
69 + "<container id=\"C1\" searchable=\"0\" parentID=\"0\" restricted=\"1\" childCount=\"2\">"
70 + "<dc:title>All Audio Items</dc:title><upnp:class>object.container</upnp:class>"
71 + "<upnp:writeStatus>UNKNOWN</upnp:writeStatus></container>"
72 + "<container id=\"C2\" searchable=\"0\" parentID=\"0\" restricted=\"1\" childCount=\"0\">"
73 + "<dc:title>All Image Items</dc:title><upnp:class>object.container</upnp:class>"
74 + "<upnp:writeStatus>UNKNOWN</upnp:writeStatus></container>" + RESPONSE_FOOTER;
76 private static final String SINGLE_CONTAINER = RESPONSE_HEADER
77 + "<container id=\"C11\" searchable=\"0\" parentID=\"C1\" restricted=\"1\" childCount=\"2\">"
78 + "<dc:title>Morning Music</dc:title><upnp:class>object.container</upnp:class>"
79 + "<upnp:writeStatus>UNKNOWN</upnp:writeStatus></container>" + RESPONSE_FOOTER;
81 private static final String DOUBLE_CONTAINER = RESPONSE_HEADER
82 + "<container id=\"C11\" searchable=\"0\" parentID=\"C1\" restricted=\"1\" childCount=\"2\">"
83 + "<dc:title>Morning Music</dc:title><upnp:class>object.container</upnp:class>"
84 + "<upnp:writeStatus>UNKNOWN</upnp:writeStatus></container>"
85 + "<container id=\"C12\" searchable=\"0\" parentID=\"C1\" restricted=\"1\" childCount=\"1\">"
86 + "<dc:title>Evening Music</dc:title><upnp:class>object.container</upnp:class>"
87 + "<upnp:writeStatus>UNKNOWN</upnp:writeStatus></container>" + RESPONSE_FOOTER;
89 private static final String DOUBLE_MEDIA = RESPONSE_HEADER + "<item id=\"M1\" parentID=\"C11\" restricted=\"1\">"
90 + "<dc:title>Music_01</dc:title><upnp:class>object.item.audioItem</upnp:class>"
91 + "<dc:creator>Creator_1</dc:creator>"
92 + "<res protocolInfo=\"http-get:*:audio/mpeg:*\" size=\"8054458\" importUri=\"http://MediaServerContent_0/1/M1/\">http://MediaServerContent_0/1/M1/Test_1.mp3</res>"
93 + "<upnp:writeStatus>UNKNOWN</upnp:writeStatus></item>"
94 + "<item id=\"M2\" parentID=\"C11\" restricted=\"1\">"
95 + "<dc:title>Music_02</dc:title><upnp:class>object.item.audioItem</upnp:class>"
96 + "<dc:creator>Creator_2</dc:creator>"
97 + "<res protocolInfo=\"http-get:*:audio/wav:*\" size=\"1156598\" importUri=\"http://MediaServerContent_0/3/M2/\">http://MediaServerContent_0/3/M2/Test_2.wav</res>"
98 + "<upnp:writeStatus>UNKNOWN</upnp:writeStatus></item>" + RESPONSE_FOOTER;
100 private static final String EXTRA_MEDIA = RESPONSE_HEADER + "<item id=\"M3\" parentID=\"C12\" restricted=\"1\">"
101 + "<dc:title>Extra_01</dc:title><upnp:class>object.item.audioItem</upnp:class>"
102 + "<dc:creator>Creator_3</dc:creator>"
103 + "<res protocolInfo=\"http-get:*:audio/mpeg:*\" size=\"8054458\" importUri=\"http://MediaServerContent_0/1/M3/\">http://MediaServerContent_0/1/M3/Test_3.mp3</res>"
104 + "<upnp:writeStatus>UNKNOWN</upnp:writeStatus></item>" + RESPONSE_FOOTER;
106 protected @Nullable UpnpServerHandler handler;
108 private ChannelUID rendererChannelUID = new ChannelUID(THING_UID + ":" + UPNPRENDERER);
109 private Channel rendererChannel = ChannelBuilder.create(rendererChannelUID, "String").build();
111 private ChannelUID browseChannelUID = new ChannelUID(THING_UID + ":" + BROWSE);
112 private Channel browseChannel = ChannelBuilder.create(browseChannelUID, "String").build();
114 private ChannelUID currentTitleChannelUID = new ChannelUID(THING_UID + ":" + CURRENTTITLE);
115 private Channel currentTitleChannel = ChannelBuilder.create(currentTitleChannelUID, "String").build();
117 private ChannelUID searchChannelUID = new ChannelUID(THING_UID + ":" + SEARCH);
118 private Channel searchChannel = ChannelBuilder.create(searchChannelUID, "String").build();
120 private ChannelUID playlistSelectChannelUID = new ChannelUID(THING_UID + ":" + PLAYLIST_SELECT);
121 private Channel playlistSelectChannel = ChannelBuilder.create(playlistSelectChannelUID, "String").build();
123 private ChannelUID playlistChannelUID = new ChannelUID(THING_UID + ":" + PLAYLIST);
124 private Channel playlistChannel = ChannelBuilder.create(playlistChannelUID, "String").build();
126 private ChannelUID playlistActionChannelUID = new ChannelUID(THING_UID + ":" + PLAYLIST_ACTION);
127 private Channel playlistActionChannel = ChannelBuilder.create(playlistActionChannelUID, "String").build();
129 private ConcurrentMap<String, UpnpRendererHandler> upnpRenderers = new ConcurrentHashMap<>();
132 private @Nullable UpnpRendererHandler rendererHandler;
134 private @Nullable Thing rendererThing;
138 public void setUp() {
141 // stub thing methods
142 when(thing.getUID()).thenReturn(new ThingUID("upnpcontrol", "upnpserver", "mockserver"));
143 when(thing.getLabel()).thenReturn("MockServer");
144 when(thing.getStatus()).thenReturn(ThingStatus.OFFLINE);
146 // stub upnpIOService methods for initialize
147 Map<String, String> result = new HashMap<>();
148 result.put("Result", BASE_CONTAINER);
149 when(upnpIOService.invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap())).thenReturn(result);
151 // stub rendererHandler, so that only one protocol is supported and results should be filtered when filter true
152 when(rendererHandler.getSink()).thenReturn(Arrays.asList("http-get:*:audio/mpeg:*"));
153 when(rendererHandler.getThing()).thenReturn(requireNonNull(rendererThing));
154 when(rendererThing.getUID()).thenReturn(new ThingUID("upnpcontrol", "upnprenderer", "mockrenderer"));
155 when(rendererThing.getLabel()).thenReturn("MockRenderer");
156 upnpRenderers.put(rendererThing.getUID().toString(), requireNonNull(rendererHandler));
159 when(thing.getChannel(UPNPRENDERER)).thenReturn(rendererChannel);
160 when(thing.getChannel(BROWSE)).thenReturn(browseChannel);
161 when(thing.getChannel(CURRENTTITLE)).thenReturn(currentTitleChannel);
162 when(thing.getChannel(SEARCH)).thenReturn(searchChannel);
163 when(thing.getChannel(PLAYLIST_SELECT)).thenReturn(playlistSelectChannel);
164 when(thing.getChannel(PLAYLIST)).thenReturn(playlistChannel);
165 when(thing.getChannel(PLAYLIST_ACTION)).thenReturn(playlistActionChannel);
167 // stub config for initialize
168 when(config.as(UpnpControlServerConfiguration.class)).thenReturn(new UpnpControlServerConfiguration());
170 handler = spy(new UpnpServerHandler(requireNonNull(thing), requireNonNull(upnpIOService),
171 requireNonNull(upnpRenderers), requireNonNull(upnpStateDescriptionProvider),
172 requireNonNull(upnpCommandDescriptionProvider), configuration));
174 initHandler(requireNonNull(handler));
176 handler.initialize();
181 public void tearDown() {
188 public void testBase() {
189 logger.info("testBase");
191 handler.config.filter = false;
192 handler.config.browseDown = false;
193 handler.config.searchFromRoot = false;
195 // Check currentEntry
196 assertThat(handler.currentEntry.getId(), is(UpnpServerHandler.DIRECTORY_ROOT));
199 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
200 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
201 assertThat(stringCaptor.getValue(), is(StringType.valueOf("0")));
203 // Check CURRENTTITLE
204 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
205 stringCaptor.capture());
206 assertThat(stringCaptor.getValue(), is(StringType.valueOf("")));
209 assertThat(handler.entries.size(), is(2));
210 assertThat(handler.entries.get(0).getId(), is("C1"));
211 assertThat(handler.entries.get(0).getTitle(), is("All Audio Items"));
212 assertThat(handler.entries.get(1).getId(), is("C2"));
213 assertThat(handler.entries.get(1).getTitle(), is("All Image Items"));
215 // Check that BROWSE channel gets the correct command options, no UP should be added
216 ArgumentCaptor<List<StateOption>> commandOptionListCaptor = ArgumentCaptor.forClass(List.class);
217 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
218 commandOptionListCaptor.capture());
219 assertThat(commandOptionListCaptor.getValue().size(), is(2));
220 assertThat(commandOptionListCaptor.getValue().get(0).getValue(), is("C1"));
221 assertThat(commandOptionListCaptor.getValue().get(0).getLabel(), is("All Audio Items"));
222 assertThat(commandOptionListCaptor.getValue().get(1).getValue(), is("C2"));
223 assertThat(commandOptionListCaptor.getValue().get(1).getLabel(), is("All Image Items"));
225 // Check media queue serving
226 verify(rendererHandler, times(0)).registerQueue(any());
230 public void testSetBrowse() {
231 logger.info("testSetBrowse");
233 handler.config.filter = false;
234 handler.config.browseDown = false;
235 handler.config.searchFromRoot = false;
237 Map<String, String> result = new HashMap<>();
238 result.put("Result", DOUBLE_MEDIA);
239 doReturn(result).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
241 handler.handleCommand(browseChannelUID, StringType.valueOf("C11"));
243 // Check currentEntry
244 assertThat(handler.currentEntry.getId(), is("C11"));
247 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
248 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
249 assertThat(stringCaptor.getValue(), is(StringType.valueOf("C11")));
251 // Check CURRENTTITLE
252 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
253 stringCaptor.capture());
254 assertThat(stringCaptor.getValue(), is(StringType.valueOf("")));
257 assertThat(handler.entries.size(), is(2));
258 assertThat(handler.entries.get(0).getId(), is("M1"));
259 assertThat(handler.entries.get(0).getTitle(), is("Music_01"));
260 assertThat(handler.entries.get(1).getId(), is("M2"));
261 assertThat(handler.entries.get(1).getTitle(), is("Music_02"));
263 // Check that BROWSE channel gets the correct state options
264 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
265 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
266 stateOptionListCaptor.capture());
267 assertThat(stateOptionListCaptor.getValue().size(), is(3));
268 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
269 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
270 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("M1"));
271 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Music_01"));
272 assertThat(stateOptionListCaptor.getValue().get(2).getValue(), is("M2"));
273 assertThat(stateOptionListCaptor.getValue().get(2).getLabel(), is("Music_02"));
275 // Check media queue serving
276 verify(rendererHandler, times(0)).registerQueue(any());
280 public void testSetBrowseRendererFilter() {
281 logger.info("testSetBrowseRendererFilter");
283 handler.config.filter = true;
284 handler.config.browseDown = false;
285 handler.config.searchFromRoot = false;
287 handler.handleCommand(rendererChannelUID, StringType.valueOf(rendererThing.getUID().toString()));
289 Map<String, String> result = new HashMap<>();
290 result.put("Result", DOUBLE_MEDIA);
291 doReturn(result).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
293 handler.handleCommand(browseChannelUID, StringType.valueOf("C11"));
295 // Check currentEntry
296 assertThat(handler.currentEntry.getId(), is("C11"));
299 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
300 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
301 assertThat(stringCaptor.getValue(), is(StringType.valueOf("C11")));
303 // Check CURRENTTITLE
304 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
305 stringCaptor.capture());
306 assertThat(stringCaptor.getValue(), is(StringType.valueOf("")));
309 assertThat(handler.entries.size(), is(1));
310 assertThat(handler.entries.get(0).getId(), is("M1"));
311 assertThat(handler.entries.get(0).getTitle(), is("Music_01"));
313 // Check that BROWSE channel gets the correct state options
314 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
315 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
316 stateOptionListCaptor.capture());
317 assertThat(stateOptionListCaptor.getValue().size(), is(2));
318 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
319 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
320 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("M1"));
321 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Music_01"));
323 // Check media queue serving
324 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(UPNPRENDERER).getUID()),
325 stringCaptor.capture());
326 assertThat(stringCaptor.getValue(), is(StringType.valueOf(rendererThing.getUID().toString())));
328 // Check media queue serving
329 verify(rendererHandler).registerQueue(any());
333 public void testBrowseContainers() {
334 logger.info("testBrowseContainers");
336 handler.config.filter = false;
337 handler.config.browseDown = false;
338 handler.config.searchFromRoot = false;
340 Map<String, String> result = new HashMap<>();
341 result.put("Result", DOUBLE_CONTAINER);
342 doReturn(result).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
344 handler.handleCommand(browseChannelUID, StringType.valueOf("C1"));
346 // Check currentEntry
347 assertThat(handler.currentEntry.getId(), is("C1"));
350 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
351 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
352 assertThat(stringCaptor.getValue(), is(StringType.valueOf("C1")));
354 // Check CURRENTTITLE
355 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
356 stringCaptor.capture());
357 assertThat(stringCaptor.getValue(), is(StringType.valueOf("All Audio Items")));
360 assertThat(handler.entries.size(), is(2));
361 assertThat(handler.entries.get(0).getId(), is("C11"));
362 assertThat(handler.entries.get(0).getTitle(), is("Morning Music"));
363 assertThat(handler.entries.get(1).getId(), is("C12"));
364 assertThat(handler.entries.get(1).getTitle(), is("Evening Music"));
366 // Check that BROWSE channel gets the correct state options
367 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
368 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
369 stateOptionListCaptor.capture());
370 assertThat(stateOptionListCaptor.getValue().size(), is(3));
371 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
372 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
373 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("C11"));
374 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Morning Music"));
375 assertThat(stateOptionListCaptor.getValue().get(2).getValue(), is("C12"));
376 assertThat(stateOptionListCaptor.getValue().get(2).getLabel(), is("Evening Music"));
378 // Check media queue serving
379 verify(rendererHandler, times(0)).registerQueue(any());
383 public void testBrowseOneContainerNoBrowseDown() {
384 logger.info("testBrowseOneContainerNoBrowseDown");
386 handler.config.filter = false;
387 handler.config.browseDown = false;
388 handler.config.searchFromRoot = false;
390 Map<String, String> resultContainer = new HashMap<>();
391 resultContainer.put("Result", SINGLE_CONTAINER);
392 Map<String, String> resultMedia = new HashMap<>();
393 resultMedia.put("Result", DOUBLE_MEDIA);
394 doReturn(resultContainer).doReturn(resultMedia).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"),
395 eq("Browse"), anyMap());
397 handler.handleCommand(browseChannelUID, StringType.valueOf("C1"));
399 // Check currentEntry
400 assertThat(handler.currentEntry.getId(), is("C1"));
403 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
404 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
405 assertThat(stringCaptor.getValue(), is(StringType.valueOf("C1")));
407 // Check CURRENTTITLE
408 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
409 stringCaptor.capture());
410 assertThat(stringCaptor.getValue(), is(StringType.valueOf("All Audio Items")));
413 assertThat(handler.entries.size(), is(1));
414 assertThat(handler.entries.get(0).getId(), is("C11"));
415 assertThat(handler.entries.get(0).getTitle(), is("Morning Music"));
417 // Check that BROWSE channel gets the correct state options
418 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
419 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
420 stateOptionListCaptor.capture());
421 assertThat(stateOptionListCaptor.getValue().size(), is(2));
422 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
423 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
424 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("C11"));
425 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Morning Music"));
427 // Check that a no media queue is being served as there is no renderer selected
428 verify(rendererHandler, times(0)).registerQueue(any());
432 public void testBrowseOneContainerBrowseDown() {
433 logger.info("testBrowseOneContainerBrowseDown");
435 handler.config.filter = false;
436 handler.config.browseDown = true;
437 handler.config.searchFromRoot = false;
439 Map<String, String> resultContainer = new HashMap<>();
440 resultContainer.put("Result", SINGLE_CONTAINER);
441 Map<String, String> resultMedia = new HashMap<>();
442 resultMedia.put("Result", DOUBLE_MEDIA);
443 doReturn(resultContainer).doReturn(resultMedia).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"),
444 eq("Browse"), anyMap());
446 handler.handleCommand(browseChannelUID, StringType.valueOf("C1"));
448 // Check currentEntry
449 assertThat(handler.currentEntry.getId(), is("C11"));
452 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
453 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
454 assertThat(stringCaptor.getValue(), is(StringType.valueOf("C11")));
456 // Check CURRENTTITLE
457 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
458 stringCaptor.capture());
459 assertThat(stringCaptor.getValue(), is(StringType.valueOf("Morning Music")));
462 assertThat(handler.entries.size(), is(2));
463 assertThat(handler.entries.get(0).getId(), is("M1"));
464 assertThat(handler.entries.get(0).getTitle(), is("Music_01"));
465 assertThat(handler.entries.get(1).getId(), is("M2"));
466 assertThat(handler.entries.get(1).getTitle(), is("Music_02"));
468 // Check that BROWSE channel gets the correct state options
469 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
470 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
471 stateOptionListCaptor.capture());
472 assertThat(stateOptionListCaptor.getValue().size(), is(3));
473 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
474 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
475 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("M1"));
476 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Music_01"));
477 assertThat(stateOptionListCaptor.getValue().get(2).getValue(), is("M2"));
478 assertThat(stateOptionListCaptor.getValue().get(2).getLabel(), is("Music_02"));
480 // Check media queue serving
481 verify(rendererHandler, times(0)).registerQueue(any());
485 public void testSearchOneContainerNotFromRootNoBrowseDown() {
486 logger.info("testSearchOneContainerNotFromRootNoBrowseDown");
488 handler.config.filter = false;
489 handler.config.browseDown = false;
490 handler.config.searchFromRoot = false;
492 // First navigate away from root
493 Map<String, String> result = new HashMap<>();
494 result.put("Result", DOUBLE_CONTAINER);
495 doReturn(result).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
496 handler.handleCommand(browseChannelUID, StringType.valueOf("C1"));
498 Map<String, String> resultContainer = new HashMap<>();
499 resultContainer.put("Result", SINGLE_CONTAINER);
500 Map<String, String> resultMedia = new HashMap<>();
501 resultMedia.put("Result", DOUBLE_MEDIA);
502 doReturn(resultContainer).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Search"),
504 doReturn(resultMedia).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
506 String searchString = "dc:title contains \"Morning\" and upnp:class derivedfrom \"object.container\"";
507 handler.handleCommand(searchChannelUID, StringType.valueOf(searchString));
509 // Check currentEntry
510 assertThat(handler.currentEntry.getId(), is("C1"));
513 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
514 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
515 assertThat(stringCaptor.getValue(), is(StringType.valueOf("C1")));
517 // Check CURRENTTITLE
518 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
519 stringCaptor.capture());
520 assertThat(stringCaptor.getValue(), is(StringType.valueOf("All Audio Items")));
523 assertThat(handler.entries.size(), is(1));
524 assertThat(handler.entries.get(0).getId(), is("C11"));
525 assertThat(handler.entries.get(0).getTitle(), is("Morning Music"));
527 // Check that BROWSE channel gets the correct state options
528 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
529 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
530 stateOptionListCaptor.capture());
531 assertThat(stateOptionListCaptor.getValue().size(), is(2));
532 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
533 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
534 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("C11"));
535 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Morning Music"));
537 // Check that a no media queue is being served as there is no renderer selected
538 verify(rendererHandler, times(0)).registerQueue(any());
542 public void testSearchOneContainerNotFromRootBrowseDown() {
543 logger.info("testSearchOneContainerNotFromRootBrowseDown");
545 handler.config.filter = false;
546 handler.config.browseDown = true;
547 handler.config.searchFromRoot = false;
549 // First navigate away from root
550 Map<String, String> result = new HashMap<>();
551 result.put("Result", DOUBLE_CONTAINER);
552 doReturn(result).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
553 handler.handleCommand(browseChannelUID, StringType.valueOf("C1"));
555 Map<String, String> resultContainer = new HashMap<>();
556 resultContainer.put("Result", SINGLE_CONTAINER);
557 Map<String, String> resultMedia = new HashMap<>();
558 resultMedia.put("Result", DOUBLE_MEDIA);
559 doReturn(resultContainer).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Search"),
561 doReturn(resultMedia).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
563 String searchString = "dc:title contains \"Morning\" and upnp:class derivedfrom \"object.container\"";
564 handler.handleCommand(searchChannelUID, StringType.valueOf(searchString));
566 // Check currentEntry
567 assertThat(handler.currentEntry.getId(), is("C11"));
570 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
571 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
572 assertThat(stringCaptor.getValue(), is(StringType.valueOf("C11")));
574 // Check CURRENTTITLE
575 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
576 stringCaptor.capture());
577 assertThat(stringCaptor.getValue(), is(StringType.valueOf("Morning Music")));
580 assertThat(handler.entries.size(), is(2));
581 assertThat(handler.entries.get(0).getId(), is("M1"));
582 assertThat(handler.entries.get(0).getTitle(), is("Music_01"));
583 assertThat(handler.entries.get(1).getId(), is("M2"));
584 assertThat(handler.entries.get(1).getTitle(), is("Music_02"));
586 // Check that BROWSE channel gets the correct state options
587 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
588 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
589 stateOptionListCaptor.capture());
590 assertThat(stateOptionListCaptor.getValue().size(), is(3));
591 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
592 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
593 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("M1"));
594 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Music_01"));
595 assertThat(stateOptionListCaptor.getValue().get(2).getValue(), is("M2"));
596 assertThat(stateOptionListCaptor.getValue().get(2).getLabel(), is("Music_02"));
598 // Check that a no media queue is being served as there is no renderer selected
599 verify(rendererHandler, times(0)).registerQueue(any());
603 public void testSearchOneContainerFromRootNoBrowseDown() {
604 logger.info("testSearchOneContainerFromRootNoBrowseDown");
606 handler.config.filter = false;
607 handler.config.browseDown = false;
608 handler.config.searchFromRoot = true;
610 // First navigate away from root
611 Map<String, String> result = new HashMap<>();
612 result.put("Result", DOUBLE_CONTAINER);
613 doReturn(result).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
614 handler.handleCommand(browseChannelUID, StringType.valueOf("C1"));
616 Map<String, String> resultContainer = new HashMap<>();
617 resultContainer.put("Result", SINGLE_CONTAINER);
618 Map<String, String> resultMedia = new HashMap<>();
619 resultMedia.put("Result", DOUBLE_MEDIA);
620 doReturn(resultContainer).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Search"),
622 doReturn(resultMedia).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
624 String searchString = "dc:title contains \"Morning\" and upnp:class derivedfrom \"object.container\"";
625 handler.handleCommand(searchChannelUID, StringType.valueOf(searchString));
627 // Check currentEntry
628 assertThat(handler.currentEntry.getId(), is("0"));
631 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
632 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
633 assertThat(stringCaptor.getValue(), is(StringType.valueOf("0")));
635 // Check CURRENTTITLE
636 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
637 stringCaptor.capture());
638 assertThat(stringCaptor.getValue(), is(StringType.valueOf("")));
641 assertThat(handler.entries.size(), is(1));
642 assertThat(handler.entries.get(0).getId(), is("C11"));
643 assertThat(handler.entries.get(0).getTitle(), is("Morning Music"));
645 // Check that BROWSE channel gets the correct state options
646 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
647 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
648 stateOptionListCaptor.capture());
649 assertThat(stateOptionListCaptor.getValue().size(), is(2));
650 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
651 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
652 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("C11"));
653 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Morning Music"));
655 // Check that a no media queue is being served as there is no renderer selected
656 verify(rendererHandler, times(0)).registerQueue(any());
660 public void testSearchOneContainerFromRootBrowseDown() {
661 logger.info("testSearchOneContainerFromRootBrowseDown");
663 handler.config.filter = false;
664 handler.config.browseDown = true;
665 handler.config.searchFromRoot = true;
667 // First navigate away from root
668 Map<String, String> result = new HashMap<>();
669 result.put("Result", DOUBLE_CONTAINER);
670 doReturn(result).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
671 handler.handleCommand(browseChannelUID, StringType.valueOf("C1"));
673 Map<String, String> resultContainer = new HashMap<>();
674 resultContainer.put("Result", SINGLE_CONTAINER);
675 Map<String, String> resultMedia = new HashMap<>();
676 resultMedia.put("Result", DOUBLE_MEDIA);
677 doReturn(resultContainer).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Search"),
679 doReturn(resultMedia).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
681 String searchString = "dc:title contains \"Morning\" and upnp:class derivedfrom \"object.container\"";
682 handler.handleCommand(searchChannelUID, StringType.valueOf(searchString));
684 // Check currentEntry
685 assertThat(handler.currentEntry.getId(), is("C11"));
688 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
689 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
690 assertThat(stringCaptor.getValue(), is(StringType.valueOf("C11")));
692 // Check CURRENTTITLE
693 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
694 stringCaptor.capture());
695 assertThat(stringCaptor.getValue(), is(StringType.valueOf("Morning Music")));
698 assertThat(handler.entries.size(), is(2));
699 assertThat(handler.entries.get(0).getId(), is("M1"));
700 assertThat(handler.entries.get(0).getTitle(), is("Music_01"));
701 assertThat(handler.entries.get(1).getId(), is("M2"));
702 assertThat(handler.entries.get(1).getTitle(), is("Music_02"));
704 // Check that BROWSE channel gets the correct state options
705 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
706 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
707 stateOptionListCaptor.capture());
708 assertThat(stateOptionListCaptor.getValue().size(), is(3));
709 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
710 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
711 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("M1"));
712 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Music_01"));
713 assertThat(stateOptionListCaptor.getValue().get(2).getValue(), is("M2"));
714 assertThat(stateOptionListCaptor.getValue().get(2).getLabel(), is("Music_02"));
716 // Check that a no media queue is being served as there is no renderer selected
717 verify(rendererHandler, times(0)).registerQueue(any());
721 public void testSearchMediaFromRootBrowseDownFilter() {
722 logger.info("testSearchMediaFromRootBrowseDownFilter");
724 handler.config.filter = true;
725 handler.config.browseDown = true;
726 handler.config.searchFromRoot = true;
728 // First navigate away from root
729 Map<String, String> result = new HashMap<>();
730 result.put("Result", DOUBLE_CONTAINER);
731 doReturn(result).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
732 handler.handleCommand(browseChannelUID, StringType.valueOf("C1"));
734 Map<String, String> resultMedia = new HashMap<>();
735 resultMedia.put("Result", DOUBLE_MEDIA);
736 doReturn(resultMedia).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Search"), anyMap());
738 String searchString = "dc:title contains \"Music\"";
739 handler.handleCommand(searchChannelUID, StringType.valueOf(searchString));
741 // Check currentEntry
742 assertThat(handler.currentEntry.getId(), is("0"));
745 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
746 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
747 assertThat(stringCaptor.getValue(), is(StringType.valueOf("0")));
749 // Check CURRENTTITLE
750 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
751 stringCaptor.capture());
752 assertThat(stringCaptor.getValue(), is(StringType.valueOf("")));
755 assertThat(handler.entries.size(), is(2));
756 assertThat(handler.entries.get(0).getId(), is("M1"));
757 assertThat(handler.entries.get(0).getTitle(), is("Music_01"));
758 assertThat(handler.entries.get(1).getId(), is("M2"));
759 assertThat(handler.entries.get(1).getTitle(), is("Music_02"));
761 // Check that BROWSE channel gets the correct state options
762 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
763 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
764 stateOptionListCaptor.capture());
765 assertThat(stateOptionListCaptor.getValue().size(), is(3));
766 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
767 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
768 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("M1"));
769 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Music_01"));
770 assertThat(stateOptionListCaptor.getValue().get(2).getValue(), is("M2"));
771 assertThat(stateOptionListCaptor.getValue().get(2).getLabel(), is("Music_02"));
773 // Check that a no media queue is being served as there is no renderer selected
774 verify(rendererHandler, times(0)).registerQueue(any());
778 public void testPlaylist() {
779 logger.info("testPlaylist");
781 handler.config.filter = false;
782 handler.config.browseDown = false;
783 handler.config.searchFromRoot = true;
785 // Check already called in initialize
786 verify(handler).playlistsListChanged();
788 // First search for media
789 Map<String, String> resultMedia = new HashMap<>();
790 resultMedia.put("Result", DOUBLE_MEDIA);
791 doReturn(resultMedia).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Search"), anyMap());
792 String searchString = "dc:title contains \"Music\"";
793 handler.handleCommand(searchChannelUID, StringType.valueOf(searchString));
796 handler.handleCommand(playlistChannelUID, StringType.valueOf("Test_Playlist"));
797 handler.handleCommand(playlistActionChannelUID, StringType.valueOf("SAVE"));
799 // Check called after saving playlist
800 verify(handler, times(2)).playlistsListChanged();
802 // Check that PLAYLIST_SELECT channel now has the playlist as a state option
803 ArgumentCaptor<List<CommandOption>> commandOptionListCaptor = ArgumentCaptor.forClass(List.class);
804 verify(handler, atLeastOnce()).updateCommandDescription(eq(thing.getChannel(PLAYLIST_SELECT).getUID()),
805 commandOptionListCaptor.capture());
806 assertThat(commandOptionListCaptor.getValue().size(), is(1));
807 assertThat(commandOptionListCaptor.getValue().get(0).getCommand(), is("Test_Playlist"));
808 assertThat(commandOptionListCaptor.getValue().get(0).getLabel(), is("Test_Playlist"));
810 // Clear PLAYLIST channel
811 handler.handleCommand(playlistChannelUID, StringType.valueOf(""));
813 // Search for some extra media
814 resultMedia = new HashMap<>();
815 resultMedia.put("Result", EXTRA_MEDIA);
816 doReturn(resultMedia).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Search"), anyMap());
817 searchString = "dc:title contains \"Extra\"";
818 handler.handleCommand(searchChannelUID, StringType.valueOf(searchString));
820 // Append to playlist
821 handler.handleCommand(playlistSelectChannelUID, StringType.valueOf("Test_Playlist"));
822 handler.handleCommand(playlistActionChannelUID, StringType.valueOf("APPEND"));
824 // Check called after appending to playlist
825 verify(handler, times(3)).playlistsListChanged();
827 // Check that PLAYLIST channel received "Test_Playlist"
828 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
829 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(PLAYLIST).getUID()), stringCaptor.capture());
830 assertThat(stringCaptor.getValue(), is(StringType.valueOf("Test_Playlist")));
832 // Clear PLAYLIST channel
833 handler.handleCommand(playlistChannelUID, StringType.valueOf(""));
836 handler.handleCommand(playlistSelectChannelUID, StringType.valueOf("Test_Playlist"));
837 handler.handleCommand(playlistActionChannelUID, StringType.valueOf("RESTORE"));
839 // Check currentEntry
840 assertThat(handler.currentEntry.getId(), is("C11"));
843 assertThat(handler.entries.size(), is(3));
844 assertThat(handler.entries.get(0).getId(), is("M1"));
845 assertThat(handler.entries.get(0).getTitle(), is("Music_01"));
846 assertThat(handler.entries.get(1).getId(), is("M2"));
847 assertThat(handler.entries.get(1).getTitle(), is("Music_02"));
848 assertThat(handler.entries.get(2).getId(), is("M3"));
849 assertThat(handler.entries.get(2).getTitle(), is("Extra_01"));
852 handler.handleCommand(playlistSelectChannelUID, StringType.valueOf("Test_Playlist"));
853 handler.handleCommand(playlistActionChannelUID, StringType.valueOf("DELETE"));
855 // Check called after deleting playlist
856 verify(handler, times(4)).playlistsListChanged();
858 // Check that PLAYLIST_SELECT channel is empty again
859 commandOptionListCaptor = ArgumentCaptor.forClass(List.class);
860 verify(handler, atLeastOnce()).updateCommandDescription(eq(thing.getChannel(PLAYLIST_SELECT).getUID()),
861 commandOptionListCaptor.capture());
862 assertThat(commandOptionListCaptor.getValue().size(), is(0));
864 // select a renderer, so we expect the "current" playlist to be created
865 handler.handleCommand(rendererChannelUID, StringType.valueOf(rendererThing.getUID().toString()));
867 // Check called after selecting renderer
868 verify(handler, times(5)).playlistsListChanged();
870 // Check that PLAYLIST_SELECT channel received "current" playlist
871 verify(handler, atLeastOnce()).updateCommandDescription(eq(thing.getChannel(PLAYLIST_SELECT).getUID()),
872 commandOptionListCaptor.capture());
873 assertThat(commandOptionListCaptor.getValue().size(), is(1));
874 assertThat(commandOptionListCaptor.getValue().get(0).getCommand(), is("current"));
875 assertThat(commandOptionListCaptor.getValue().get(0).getLabel(), is("current"));