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 = """
64 <DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" \
65 xmlns:dc="http://purl.org/dc/elements/1.1/" \
66 xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">\
68 private static final String RESPONSE_FOOTER = "</DIDL-Lite>";
70 private static final String BASE_CONTAINER = RESPONSE_HEADER
71 + "<container id=\"C1\" searchable=\"0\" parentID=\"0\" restricted=\"1\" childCount=\"2\">"
72 + "<dc:title>All Audio Items</dc:title><upnp:class>object.container</upnp:class>"
73 + "<upnp:writeStatus>UNKNOWN</upnp:writeStatus></container>"
74 + "<container id=\"C2\" searchable=\"0\" parentID=\"0\" restricted=\"1\" childCount=\"0\">"
75 + "<dc:title>All Image Items</dc:title><upnp:class>object.container</upnp:class>"
76 + "<upnp:writeStatus>UNKNOWN</upnp:writeStatus></container>" + RESPONSE_FOOTER;
78 private static final String SINGLE_CONTAINER = RESPONSE_HEADER
79 + "<container id=\"C11\" searchable=\"0\" parentID=\"C1\" restricted=\"1\" childCount=\"2\">"
80 + "<dc:title>Morning Music</dc:title><upnp:class>object.container</upnp:class>"
81 + "<upnp:writeStatus>UNKNOWN</upnp:writeStatus></container>" + RESPONSE_FOOTER;
83 private static final String DOUBLE_CONTAINER = RESPONSE_HEADER
84 + "<container id=\"C11\" searchable=\"0\" parentID=\"C1\" restricted=\"1\" childCount=\"2\">"
85 + "<dc:title>Morning Music</dc:title><upnp:class>object.container</upnp:class>"
86 + "<upnp:writeStatus>UNKNOWN</upnp:writeStatus></container>"
87 + "<container id=\"C12\" searchable=\"0\" parentID=\"C1\" restricted=\"1\" childCount=\"1\">"
88 + "<dc:title>Evening Music</dc:title><upnp:class>object.container</upnp:class>"
89 + "<upnp:writeStatus>UNKNOWN</upnp:writeStatus></container>" + RESPONSE_FOOTER;
91 private static final String DOUBLE_MEDIA = RESPONSE_HEADER + "<item id=\"M1\" parentID=\"C11\" restricted=\"1\">"
92 + "<dc:title>Music_01</dc:title><upnp:class>object.item.audioItem</upnp:class>"
93 + "<dc:creator>Creator_1</dc:creator>"
94 + "<res protocolInfo=\"http-get:*:audio/mpeg:*\" size=\"8054458\" importUri=\"http://MediaServerContent_0/1/M1/\">http://MediaServerContent_0/1/M1/Test_1.mp3</res>"
95 + "<upnp:writeStatus>UNKNOWN</upnp:writeStatus></item>"
96 + "<item id=\"M2\" parentID=\"C11\" restricted=\"1\">"
97 + "<dc:title>Music_02</dc:title><upnp:class>object.item.audioItem</upnp:class>"
98 + "<dc:creator>Creator_2</dc:creator>"
99 + "<res protocolInfo=\"http-get:*:audio/wav:*\" size=\"1156598\" importUri=\"http://MediaServerContent_0/3/M2/\">http://MediaServerContent_0/3/M2/Test_2.wav</res>"
100 + "<upnp:writeStatus>UNKNOWN</upnp:writeStatus></item>" + RESPONSE_FOOTER;
102 private static final String EXTRA_MEDIA = RESPONSE_HEADER + "<item id=\"M3\" parentID=\"C12\" restricted=\"1\">"
103 + "<dc:title>Extra_01</dc:title><upnp:class>object.item.audioItem</upnp:class>"
104 + "<dc:creator>Creator_3</dc:creator>"
105 + "<res protocolInfo=\"http-get:*:audio/mpeg:*\" size=\"8054458\" importUri=\"http://MediaServerContent_0/1/M3/\">http://MediaServerContent_0/1/M3/Test_3.mp3</res>"
106 + "<upnp:writeStatus>UNKNOWN</upnp:writeStatus></item>" + RESPONSE_FOOTER;
108 protected @Nullable UpnpServerHandler handler;
110 private ChannelUID rendererChannelUID = new ChannelUID(THING_UID + ":" + UPNPRENDERER);
111 private Channel rendererChannel = ChannelBuilder.create(rendererChannelUID, "String").build();
113 private ChannelUID browseChannelUID = new ChannelUID(THING_UID + ":" + BROWSE);
114 private Channel browseChannel = ChannelBuilder.create(browseChannelUID, "String").build();
116 private ChannelUID currentTitleChannelUID = new ChannelUID(THING_UID + ":" + CURRENTTITLE);
117 private Channel currentTitleChannel = ChannelBuilder.create(currentTitleChannelUID, "String").build();
119 private ChannelUID searchChannelUID = new ChannelUID(THING_UID + ":" + SEARCH);
120 private Channel searchChannel = ChannelBuilder.create(searchChannelUID, "String").build();
122 private ChannelUID playlistSelectChannelUID = new ChannelUID(THING_UID + ":" + PLAYLIST_SELECT);
123 private Channel playlistSelectChannel = ChannelBuilder.create(playlistSelectChannelUID, "String").build();
125 private ChannelUID playlistChannelUID = new ChannelUID(THING_UID + ":" + PLAYLIST);
126 private Channel playlistChannel = ChannelBuilder.create(playlistChannelUID, "String").build();
128 private ChannelUID playlistActionChannelUID = new ChannelUID(THING_UID + ":" + PLAYLIST_ACTION);
129 private Channel playlistActionChannel = ChannelBuilder.create(playlistActionChannelUID, "String").build();
131 private ConcurrentMap<String, UpnpRendererHandler> upnpRenderers = new ConcurrentHashMap<>();
134 private @Nullable UpnpRendererHandler rendererHandler;
136 private @Nullable Thing rendererThing;
140 public void setUp() {
143 // stub thing methods
144 when(thing.getUID()).thenReturn(new ThingUID("upnpcontrol", "upnpserver", "mockserver"));
145 when(thing.getLabel()).thenReturn("MockServer");
146 when(thing.getStatus()).thenReturn(ThingStatus.OFFLINE);
148 // stub upnpIOService methods for initialize
149 Map<String, String> result = new HashMap<>();
150 result.put("Result", BASE_CONTAINER);
151 when(upnpIOService.invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap())).thenReturn(result);
153 // stub rendererHandler, so that only one protocol is supported and results should be filtered when filter true
154 when(rendererHandler.getSink()).thenReturn(Arrays.asList("http-get:*:audio/mpeg:*"));
155 when(rendererHandler.getThing()).thenReturn(requireNonNull(rendererThing));
156 when(rendererThing.getUID()).thenReturn(new ThingUID("upnpcontrol", "upnprenderer", "mockrenderer"));
157 when(rendererThing.getLabel()).thenReturn("MockRenderer");
158 upnpRenderers.put(rendererThing.getUID().toString(), requireNonNull(rendererHandler));
161 when(thing.getChannel(UPNPRENDERER)).thenReturn(rendererChannel);
162 when(thing.getChannel(BROWSE)).thenReturn(browseChannel);
163 when(thing.getChannel(CURRENTTITLE)).thenReturn(currentTitleChannel);
164 when(thing.getChannel(SEARCH)).thenReturn(searchChannel);
165 when(thing.getChannel(PLAYLIST_SELECT)).thenReturn(playlistSelectChannel);
166 when(thing.getChannel(PLAYLIST)).thenReturn(playlistChannel);
167 when(thing.getChannel(PLAYLIST_ACTION)).thenReturn(playlistActionChannel);
169 // stub config for initialize
170 when(config.as(UpnpControlServerConfiguration.class)).thenReturn(new UpnpControlServerConfiguration());
172 handler = spy(new UpnpServerHandler(requireNonNull(thing), requireNonNull(upnpIOService),
173 requireNonNull(upnpRenderers), requireNonNull(upnpStateDescriptionProvider),
174 requireNonNull(upnpCommandDescriptionProvider), configuration));
176 initHandler(requireNonNull(handler));
178 handler.initialize();
183 public void tearDown() {
190 public void testBase() {
191 logger.info("testBase");
193 handler.config.filter = false;
194 handler.config.browseDown = false;
195 handler.config.searchFromRoot = false;
197 // Check currentEntry
198 assertThat(handler.currentEntry.getId(), is(UpnpServerHandler.DIRECTORY_ROOT));
201 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
202 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
203 assertThat(stringCaptor.getValue(), is(StringType.valueOf("0")));
205 // Check CURRENTTITLE
206 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
207 stringCaptor.capture());
208 assertThat(stringCaptor.getValue(), is(StringType.valueOf("")));
211 assertThat(handler.entries.size(), is(2));
212 assertThat(handler.entries.get(0).getId(), is("C1"));
213 assertThat(handler.entries.get(0).getTitle(), is("All Audio Items"));
214 assertThat(handler.entries.get(1).getId(), is("C2"));
215 assertThat(handler.entries.get(1).getTitle(), is("All Image Items"));
217 // Check that BROWSE channel gets the correct command options, no UP should be added
218 ArgumentCaptor<List<StateOption>> commandOptionListCaptor = ArgumentCaptor.forClass(List.class);
219 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
220 commandOptionListCaptor.capture());
221 assertThat(commandOptionListCaptor.getValue().size(), is(2));
222 assertThat(commandOptionListCaptor.getValue().get(0).getValue(), is("C1"));
223 assertThat(commandOptionListCaptor.getValue().get(0).getLabel(), is("All Audio Items"));
224 assertThat(commandOptionListCaptor.getValue().get(1).getValue(), is("C2"));
225 assertThat(commandOptionListCaptor.getValue().get(1).getLabel(), is("All Image Items"));
227 // Check media queue serving
228 verify(rendererHandler, times(0)).registerQueue(any());
232 public void testSetBrowse() {
233 logger.info("testSetBrowse");
235 handler.config.filter = false;
236 handler.config.browseDown = false;
237 handler.config.searchFromRoot = false;
239 Map<String, String> result = new HashMap<>();
240 result.put("Result", DOUBLE_MEDIA);
241 doReturn(result).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
243 handler.handleCommand(browseChannelUID, StringType.valueOf("C11"));
245 // Check currentEntry
246 assertThat(handler.currentEntry.getId(), is("C11"));
249 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
250 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
251 assertThat(stringCaptor.getValue(), is(StringType.valueOf("C11")));
253 // Check CURRENTTITLE
254 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
255 stringCaptor.capture());
256 assertThat(stringCaptor.getValue(), is(StringType.valueOf("")));
259 assertThat(handler.entries.size(), is(2));
260 assertThat(handler.entries.get(0).getId(), is("M1"));
261 assertThat(handler.entries.get(0).getTitle(), is("Music_01"));
262 assertThat(handler.entries.get(1).getId(), is("M2"));
263 assertThat(handler.entries.get(1).getTitle(), is("Music_02"));
265 // Check that BROWSE channel gets the correct state options
266 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
267 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
268 stateOptionListCaptor.capture());
269 assertThat(stateOptionListCaptor.getValue().size(), is(3));
270 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
271 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
272 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("M1"));
273 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Music_01"));
274 assertThat(stateOptionListCaptor.getValue().get(2).getValue(), is("M2"));
275 assertThat(stateOptionListCaptor.getValue().get(2).getLabel(), is("Music_02"));
277 // Check media queue serving
278 verify(rendererHandler, times(0)).registerQueue(any());
282 public void testSetBrowseRendererFilter() {
283 logger.info("testSetBrowseRendererFilter");
285 handler.config.filter = true;
286 handler.config.browseDown = false;
287 handler.config.searchFromRoot = false;
289 handler.handleCommand(rendererChannelUID, StringType.valueOf(rendererThing.getUID().toString()));
291 Map<String, String> result = new HashMap<>();
292 result.put("Result", DOUBLE_MEDIA);
293 doReturn(result).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
295 handler.handleCommand(browseChannelUID, StringType.valueOf("C11"));
297 // Check currentEntry
298 assertThat(handler.currentEntry.getId(), is("C11"));
301 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
302 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
303 assertThat(stringCaptor.getValue(), is(StringType.valueOf("C11")));
305 // Check CURRENTTITLE
306 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
307 stringCaptor.capture());
308 assertThat(stringCaptor.getValue(), is(StringType.valueOf("")));
311 assertThat(handler.entries.size(), is(1));
312 assertThat(handler.entries.get(0).getId(), is("M1"));
313 assertThat(handler.entries.get(0).getTitle(), is("Music_01"));
315 // Check that BROWSE channel gets the correct state options
316 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
317 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
318 stateOptionListCaptor.capture());
319 assertThat(stateOptionListCaptor.getValue().size(), is(2));
320 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
321 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
322 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("M1"));
323 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Music_01"));
325 // Check media queue serving
326 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(UPNPRENDERER).getUID()),
327 stringCaptor.capture());
328 assertThat(stringCaptor.getValue(), is(StringType.valueOf(rendererThing.getUID().toString())));
330 // Check media queue serving
331 verify(rendererHandler).registerQueue(any());
335 public void testBrowseContainers() {
336 logger.info("testBrowseContainers");
338 handler.config.filter = false;
339 handler.config.browseDown = false;
340 handler.config.searchFromRoot = false;
342 Map<String, String> result = new HashMap<>();
343 result.put("Result", DOUBLE_CONTAINER);
344 doReturn(result).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
346 handler.handleCommand(browseChannelUID, StringType.valueOf("C1"));
348 // Check currentEntry
349 assertThat(handler.currentEntry.getId(), is("C1"));
352 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
353 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
354 assertThat(stringCaptor.getValue(), is(StringType.valueOf("C1")));
356 // Check CURRENTTITLE
357 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
358 stringCaptor.capture());
359 assertThat(stringCaptor.getValue(), is(StringType.valueOf("All Audio Items")));
362 assertThat(handler.entries.size(), is(2));
363 assertThat(handler.entries.get(0).getId(), is("C11"));
364 assertThat(handler.entries.get(0).getTitle(), is("Morning Music"));
365 assertThat(handler.entries.get(1).getId(), is("C12"));
366 assertThat(handler.entries.get(1).getTitle(), is("Evening Music"));
368 // Check that BROWSE channel gets the correct state options
369 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
370 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
371 stateOptionListCaptor.capture());
372 assertThat(stateOptionListCaptor.getValue().size(), is(3));
373 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
374 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
375 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("C11"));
376 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Morning Music"));
377 assertThat(stateOptionListCaptor.getValue().get(2).getValue(), is("C12"));
378 assertThat(stateOptionListCaptor.getValue().get(2).getLabel(), is("Evening Music"));
380 // Check media queue serving
381 verify(rendererHandler, times(0)).registerQueue(any());
385 public void testBrowseOneContainerNoBrowseDown() {
386 logger.info("testBrowseOneContainerNoBrowseDown");
388 handler.config.filter = false;
389 handler.config.browseDown = false;
390 handler.config.searchFromRoot = false;
392 Map<String, String> resultContainer = new HashMap<>();
393 resultContainer.put("Result", SINGLE_CONTAINER);
394 Map<String, String> resultMedia = new HashMap<>();
395 resultMedia.put("Result", DOUBLE_MEDIA);
396 doReturn(resultContainer).doReturn(resultMedia).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"),
397 eq("Browse"), anyMap());
399 handler.handleCommand(browseChannelUID, StringType.valueOf("C1"));
401 // Check currentEntry
402 assertThat(handler.currentEntry.getId(), is("C1"));
405 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
406 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
407 assertThat(stringCaptor.getValue(), is(StringType.valueOf("C1")));
409 // Check CURRENTTITLE
410 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
411 stringCaptor.capture());
412 assertThat(stringCaptor.getValue(), is(StringType.valueOf("All Audio Items")));
415 assertThat(handler.entries.size(), is(1));
416 assertThat(handler.entries.get(0).getId(), is("C11"));
417 assertThat(handler.entries.get(0).getTitle(), is("Morning Music"));
419 // Check that BROWSE channel gets the correct state options
420 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
421 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
422 stateOptionListCaptor.capture());
423 assertThat(stateOptionListCaptor.getValue().size(), is(2));
424 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
425 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
426 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("C11"));
427 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Morning Music"));
429 // Check that a no media queue is being served as there is no renderer selected
430 verify(rendererHandler, times(0)).registerQueue(any());
434 public void testBrowseOneContainerBrowseDown() {
435 logger.info("testBrowseOneContainerBrowseDown");
437 handler.config.filter = false;
438 handler.config.browseDown = true;
439 handler.config.searchFromRoot = false;
441 Map<String, String> resultContainer = new HashMap<>();
442 resultContainer.put("Result", SINGLE_CONTAINER);
443 Map<String, String> resultMedia = new HashMap<>();
444 resultMedia.put("Result", DOUBLE_MEDIA);
445 doReturn(resultContainer).doReturn(resultMedia).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"),
446 eq("Browse"), anyMap());
448 handler.handleCommand(browseChannelUID, StringType.valueOf("C1"));
450 // Check currentEntry
451 assertThat(handler.currentEntry.getId(), is("C11"));
454 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
455 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
456 assertThat(stringCaptor.getValue(), is(StringType.valueOf("C11")));
458 // Check CURRENTTITLE
459 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
460 stringCaptor.capture());
461 assertThat(stringCaptor.getValue(), is(StringType.valueOf("Morning Music")));
464 assertThat(handler.entries.size(), is(2));
465 assertThat(handler.entries.get(0).getId(), is("M1"));
466 assertThat(handler.entries.get(0).getTitle(), is("Music_01"));
467 assertThat(handler.entries.get(1).getId(), is("M2"));
468 assertThat(handler.entries.get(1).getTitle(), is("Music_02"));
470 // Check that BROWSE channel gets the correct state options
471 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
472 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
473 stateOptionListCaptor.capture());
474 assertThat(stateOptionListCaptor.getValue().size(), is(3));
475 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
476 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
477 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("M1"));
478 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Music_01"));
479 assertThat(stateOptionListCaptor.getValue().get(2).getValue(), is("M2"));
480 assertThat(stateOptionListCaptor.getValue().get(2).getLabel(), is("Music_02"));
482 // Check media queue serving
483 verify(rendererHandler, times(0)).registerQueue(any());
487 public void testSearchOneContainerNotFromRootNoBrowseDown() {
488 logger.info("testSearchOneContainerNotFromRootNoBrowseDown");
490 handler.config.filter = false;
491 handler.config.browseDown = false;
492 handler.config.searchFromRoot = false;
494 // First navigate away from root
495 Map<String, String> result = new HashMap<>();
496 result.put("Result", DOUBLE_CONTAINER);
497 doReturn(result).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
498 handler.handleCommand(browseChannelUID, StringType.valueOf("C1"));
500 Map<String, String> resultContainer = new HashMap<>();
501 resultContainer.put("Result", SINGLE_CONTAINER);
502 Map<String, String> resultMedia = new HashMap<>();
503 resultMedia.put("Result", DOUBLE_MEDIA);
504 doReturn(resultContainer).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Search"),
506 doReturn(resultMedia).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
508 String searchString = "dc:title contains \"Morning\" and upnp:class derivedfrom \"object.container\"";
509 handler.handleCommand(searchChannelUID, StringType.valueOf(searchString));
511 // Check currentEntry
512 assertThat(handler.currentEntry.getId(), is("C1"));
515 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
516 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
517 assertThat(stringCaptor.getValue(), is(StringType.valueOf("C1")));
519 // Check CURRENTTITLE
520 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
521 stringCaptor.capture());
522 assertThat(stringCaptor.getValue(), is(StringType.valueOf("All Audio Items")));
525 assertThat(handler.entries.size(), is(1));
526 assertThat(handler.entries.get(0).getId(), is("C11"));
527 assertThat(handler.entries.get(0).getTitle(), is("Morning Music"));
529 // Check that BROWSE channel gets the correct state options
530 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
531 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
532 stateOptionListCaptor.capture());
533 assertThat(stateOptionListCaptor.getValue().size(), is(2));
534 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
535 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
536 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("C11"));
537 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Morning Music"));
539 // Check that a no media queue is being served as there is no renderer selected
540 verify(rendererHandler, times(0)).registerQueue(any());
544 public void testSearchOneContainerNotFromRootBrowseDown() {
545 logger.info("testSearchOneContainerNotFromRootBrowseDown");
547 handler.config.filter = false;
548 handler.config.browseDown = true;
549 handler.config.searchFromRoot = false;
551 // First navigate away from root
552 Map<String, String> result = new HashMap<>();
553 result.put("Result", DOUBLE_CONTAINER);
554 doReturn(result).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
555 handler.handleCommand(browseChannelUID, StringType.valueOf("C1"));
557 Map<String, String> resultContainer = new HashMap<>();
558 resultContainer.put("Result", SINGLE_CONTAINER);
559 Map<String, String> resultMedia = new HashMap<>();
560 resultMedia.put("Result", DOUBLE_MEDIA);
561 doReturn(resultContainer).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Search"),
563 doReturn(resultMedia).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
565 String searchString = "dc:title contains \"Morning\" and upnp:class derivedfrom \"object.container\"";
566 handler.handleCommand(searchChannelUID, StringType.valueOf(searchString));
568 // Check currentEntry
569 assertThat(handler.currentEntry.getId(), is("C11"));
572 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
573 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
574 assertThat(stringCaptor.getValue(), is(StringType.valueOf("C11")));
576 // Check CURRENTTITLE
577 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
578 stringCaptor.capture());
579 assertThat(stringCaptor.getValue(), is(StringType.valueOf("Morning Music")));
582 assertThat(handler.entries.size(), is(2));
583 assertThat(handler.entries.get(0).getId(), is("M1"));
584 assertThat(handler.entries.get(0).getTitle(), is("Music_01"));
585 assertThat(handler.entries.get(1).getId(), is("M2"));
586 assertThat(handler.entries.get(1).getTitle(), is("Music_02"));
588 // Check that BROWSE channel gets the correct state options
589 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
590 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
591 stateOptionListCaptor.capture());
592 assertThat(stateOptionListCaptor.getValue().size(), is(3));
593 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
594 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
595 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("M1"));
596 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Music_01"));
597 assertThat(stateOptionListCaptor.getValue().get(2).getValue(), is("M2"));
598 assertThat(stateOptionListCaptor.getValue().get(2).getLabel(), is("Music_02"));
600 // Check that a no media queue is being served as there is no renderer selected
601 verify(rendererHandler, times(0)).registerQueue(any());
605 public void testSearchOneContainerFromRootNoBrowseDown() {
606 logger.info("testSearchOneContainerFromRootNoBrowseDown");
608 handler.config.filter = false;
609 handler.config.browseDown = false;
610 handler.config.searchFromRoot = true;
612 // First navigate away from root
613 Map<String, String> result = new HashMap<>();
614 result.put("Result", DOUBLE_CONTAINER);
615 doReturn(result).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
616 handler.handleCommand(browseChannelUID, StringType.valueOf("C1"));
618 Map<String, String> resultContainer = new HashMap<>();
619 resultContainer.put("Result", SINGLE_CONTAINER);
620 Map<String, String> resultMedia = new HashMap<>();
621 resultMedia.put("Result", DOUBLE_MEDIA);
622 doReturn(resultContainer).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Search"),
624 doReturn(resultMedia).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
626 String searchString = "dc:title contains \"Morning\" and upnp:class derivedfrom \"object.container\"";
627 handler.handleCommand(searchChannelUID, StringType.valueOf(searchString));
629 // Check currentEntry
630 assertThat(handler.currentEntry.getId(), is("0"));
633 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
634 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
635 assertThat(stringCaptor.getValue(), is(StringType.valueOf("0")));
637 // Check CURRENTTITLE
638 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
639 stringCaptor.capture());
640 assertThat(stringCaptor.getValue(), is(StringType.valueOf("")));
643 assertThat(handler.entries.size(), is(1));
644 assertThat(handler.entries.get(0).getId(), is("C11"));
645 assertThat(handler.entries.get(0).getTitle(), is("Morning Music"));
647 // Check that BROWSE channel gets the correct state options
648 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
649 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
650 stateOptionListCaptor.capture());
651 assertThat(stateOptionListCaptor.getValue().size(), is(2));
652 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
653 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
654 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("C11"));
655 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Morning Music"));
657 // Check that a no media queue is being served as there is no renderer selected
658 verify(rendererHandler, times(0)).registerQueue(any());
662 public void testSearchOneContainerFromRootBrowseDown() {
663 logger.info("testSearchOneContainerFromRootBrowseDown");
665 handler.config.filter = false;
666 handler.config.browseDown = true;
667 handler.config.searchFromRoot = true;
669 // First navigate away from root
670 Map<String, String> result = new HashMap<>();
671 result.put("Result", DOUBLE_CONTAINER);
672 doReturn(result).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
673 handler.handleCommand(browseChannelUID, StringType.valueOf("C1"));
675 Map<String, String> resultContainer = new HashMap<>();
676 resultContainer.put("Result", SINGLE_CONTAINER);
677 Map<String, String> resultMedia = new HashMap<>();
678 resultMedia.put("Result", DOUBLE_MEDIA);
679 doReturn(resultContainer).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Search"),
681 doReturn(resultMedia).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
683 String searchString = "dc:title contains \"Morning\" and upnp:class derivedfrom \"object.container\"";
684 handler.handleCommand(searchChannelUID, StringType.valueOf(searchString));
686 // Check currentEntry
687 assertThat(handler.currentEntry.getId(), is("C11"));
690 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
691 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
692 assertThat(stringCaptor.getValue(), is(StringType.valueOf("C11")));
694 // Check CURRENTTITLE
695 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
696 stringCaptor.capture());
697 assertThat(stringCaptor.getValue(), is(StringType.valueOf("Morning Music")));
700 assertThat(handler.entries.size(), is(2));
701 assertThat(handler.entries.get(0).getId(), is("M1"));
702 assertThat(handler.entries.get(0).getTitle(), is("Music_01"));
703 assertThat(handler.entries.get(1).getId(), is("M2"));
704 assertThat(handler.entries.get(1).getTitle(), is("Music_02"));
706 // Check that BROWSE channel gets the correct state options
707 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
708 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
709 stateOptionListCaptor.capture());
710 assertThat(stateOptionListCaptor.getValue().size(), is(3));
711 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
712 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
713 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("M1"));
714 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Music_01"));
715 assertThat(stateOptionListCaptor.getValue().get(2).getValue(), is("M2"));
716 assertThat(stateOptionListCaptor.getValue().get(2).getLabel(), is("Music_02"));
718 // Check that a no media queue is being served as there is no renderer selected
719 verify(rendererHandler, times(0)).registerQueue(any());
723 public void testSearchMediaFromRootBrowseDownFilter() {
724 logger.info("testSearchMediaFromRootBrowseDownFilter");
726 handler.config.filter = true;
727 handler.config.browseDown = true;
728 handler.config.searchFromRoot = true;
730 // First navigate away from root
731 Map<String, String> result = new HashMap<>();
732 result.put("Result", DOUBLE_CONTAINER);
733 doReturn(result).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Browse"), anyMap());
734 handler.handleCommand(browseChannelUID, StringType.valueOf("C1"));
736 Map<String, String> resultMedia = new HashMap<>();
737 resultMedia.put("Result", DOUBLE_MEDIA);
738 doReturn(resultMedia).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Search"), anyMap());
740 String searchString = "dc:title contains \"Music\"";
741 handler.handleCommand(searchChannelUID, StringType.valueOf(searchString));
743 // Check currentEntry
744 assertThat(handler.currentEntry.getId(), is("0"));
747 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
748 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(BROWSE).getUID()), stringCaptor.capture());
749 assertThat(stringCaptor.getValue(), is(StringType.valueOf("0")));
751 // Check CURRENTTITLE
752 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CURRENTTITLE).getUID()),
753 stringCaptor.capture());
754 assertThat(stringCaptor.getValue(), is(StringType.valueOf("")));
757 assertThat(handler.entries.size(), is(2));
758 assertThat(handler.entries.get(0).getId(), is("M1"));
759 assertThat(handler.entries.get(0).getTitle(), is("Music_01"));
760 assertThat(handler.entries.get(1).getId(), is("M2"));
761 assertThat(handler.entries.get(1).getTitle(), is("Music_02"));
763 // Check that BROWSE channel gets the correct state options
764 ArgumentCaptor<List<StateOption>> stateOptionListCaptor = ArgumentCaptor.forClass(List.class);
765 verify(handler, atLeastOnce()).updateStateDescription(eq(thing.getChannel(BROWSE).getUID()),
766 stateOptionListCaptor.capture());
767 assertThat(stateOptionListCaptor.getValue().size(), is(3));
768 assertThat(stateOptionListCaptor.getValue().get(0).getValue(), is(".."));
769 assertThat(stateOptionListCaptor.getValue().get(0).getLabel(), is(".."));
770 assertThat(stateOptionListCaptor.getValue().get(1).getValue(), is("M1"));
771 assertThat(stateOptionListCaptor.getValue().get(1).getLabel(), is("Music_01"));
772 assertThat(stateOptionListCaptor.getValue().get(2).getValue(), is("M2"));
773 assertThat(stateOptionListCaptor.getValue().get(2).getLabel(), is("Music_02"));
775 // Check that a no media queue is being served as there is no renderer selected
776 verify(rendererHandler, times(0)).registerQueue(any());
780 public void testPlaylist() {
781 logger.info("testPlaylist");
783 handler.config.filter = false;
784 handler.config.browseDown = false;
785 handler.config.searchFromRoot = true;
787 // Check already called in initialize
788 verify(handler).playlistsListChanged();
790 // First search for media
791 Map<String, String> resultMedia = new HashMap<>();
792 resultMedia.put("Result", DOUBLE_MEDIA);
793 doReturn(resultMedia).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Search"), anyMap());
794 String searchString = "dc:title contains \"Music\"";
795 handler.handleCommand(searchChannelUID, StringType.valueOf(searchString));
798 handler.handleCommand(playlistChannelUID, StringType.valueOf("Test_Playlist"));
799 handler.handleCommand(playlistActionChannelUID, StringType.valueOf("SAVE"));
801 // Check called after saving playlist
802 verify(handler, times(2)).playlistsListChanged();
804 // Check that PLAYLIST_SELECT channel now has the playlist as a state option
805 ArgumentCaptor<List<CommandOption>> commandOptionListCaptor = ArgumentCaptor.forClass(List.class);
806 verify(handler, atLeastOnce()).updateCommandDescription(eq(thing.getChannel(PLAYLIST_SELECT).getUID()),
807 commandOptionListCaptor.capture());
808 assertThat(commandOptionListCaptor.getValue().size(), is(1));
809 assertThat(commandOptionListCaptor.getValue().get(0).getCommand(), is("Test_Playlist"));
810 assertThat(commandOptionListCaptor.getValue().get(0).getLabel(), is("Test_Playlist"));
812 // Clear PLAYLIST channel
813 handler.handleCommand(playlistChannelUID, StringType.valueOf(""));
815 // Search for some extra media
816 resultMedia = new HashMap<>();
817 resultMedia.put("Result", EXTRA_MEDIA);
818 doReturn(resultMedia).when(upnpIOService).invokeAction(any(), eq("ContentDirectory"), eq("Search"), anyMap());
819 searchString = "dc:title contains \"Extra\"";
820 handler.handleCommand(searchChannelUID, StringType.valueOf(searchString));
822 // Append to playlist
823 handler.handleCommand(playlistSelectChannelUID, StringType.valueOf("Test_Playlist"));
824 handler.handleCommand(playlistActionChannelUID, StringType.valueOf("APPEND"));
826 // Check called after appending to playlist
827 verify(handler, times(3)).playlistsListChanged();
829 // Check that PLAYLIST channel received "Test_Playlist"
830 ArgumentCaptor<StringType> stringCaptor = ArgumentCaptor.forClass(StringType.class);
831 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(PLAYLIST).getUID()), stringCaptor.capture());
832 assertThat(stringCaptor.getValue(), is(StringType.valueOf("Test_Playlist")));
834 // Clear PLAYLIST channel
835 handler.handleCommand(playlistChannelUID, StringType.valueOf(""));
838 handler.handleCommand(playlistSelectChannelUID, StringType.valueOf("Test_Playlist"));
839 handler.handleCommand(playlistActionChannelUID, StringType.valueOf("RESTORE"));
841 // Check currentEntry
842 assertThat(handler.currentEntry.getId(), is("C11"));
845 assertThat(handler.entries.size(), is(3));
846 assertThat(handler.entries.get(0).getId(), is("M1"));
847 assertThat(handler.entries.get(0).getTitle(), is("Music_01"));
848 assertThat(handler.entries.get(1).getId(), is("M2"));
849 assertThat(handler.entries.get(1).getTitle(), is("Music_02"));
850 assertThat(handler.entries.get(2).getId(), is("M3"));
851 assertThat(handler.entries.get(2).getTitle(), is("Extra_01"));
854 handler.handleCommand(playlistSelectChannelUID, StringType.valueOf("Test_Playlist"));
855 handler.handleCommand(playlistActionChannelUID, StringType.valueOf("DELETE"));
857 // Check called after deleting playlist
858 verify(handler, times(4)).playlistsListChanged();
860 // Check that PLAYLIST_SELECT channel is empty again
861 commandOptionListCaptor = ArgumentCaptor.forClass(List.class);
862 verify(handler, atLeastOnce()).updateCommandDescription(eq(thing.getChannel(PLAYLIST_SELECT).getUID()),
863 commandOptionListCaptor.capture());
864 assertThat(commandOptionListCaptor.getValue().size(), is(0));
866 // select a renderer, so we expect the "current" playlist to be created
867 handler.handleCommand(rendererChannelUID, StringType.valueOf(rendererThing.getUID().toString()));
869 // Check called after selecting renderer
870 verify(handler, times(5)).playlistsListChanged();
872 // Check that PLAYLIST_SELECT channel received "current" playlist
873 verify(handler, atLeastOnce()).updateCommandDescription(eq(thing.getChannel(PLAYLIST_SELECT).getUID()),
874 commandOptionListCaptor.capture());
875 assertThat(commandOptionListCaptor.getValue().size(), is(1));
876 assertThat(commandOptionListCaptor.getValue().get(0).getCommand(), is("current"));
877 assertThat(commandOptionListCaptor.getValue().get(0).getLabel(), is("current"));