[bosesoundtouch] Fix regression and add tests (#14097)

* Fix regression and add tests

Signed-off-by: lsiepel <leosiepel@gmail.com>
This commit is contained in:
lsiepel 2022-12-30 13:58:27 +01:00 committed by Jacob Laursen
parent 390c790936
commit 8c175ba4bc
6 changed files with 221 additions and 17 deletions

View File

@ -155,7 +155,6 @@ public class CommandExecutor implements AvailableSources {
contentItem.setPresetID(presetID);
currentContentItem = contentItem;
}
updateOperatingValues();
}

View File

@ -97,11 +97,6 @@ public class XMLResponseHandler extends DefaultHandler {
// showing a
// warning for unhandled states
XMLHandlerState localState = null;
if (stateMap != null) {
localState = stateMap.get(localName);
}
switch (curState) {
case INIT:
if ("updates".equals(localName)) {
@ -112,10 +107,13 @@ public class XMLResponseHandler extends DefaultHandler {
state = XMLHandlerState.Unprocessed;
}
} else {
XMLHandlerState localState = stateMap.get(localName);
if (localState == null) {
logger.debug("{}: Unhandled XML entity during {}: '{}", handler.getDeviceName(), curState,
logger.warn("{}: Unhandled XML entity during {}: '{}", handler.getDeviceName(), curState,
localName);
state = XMLHandlerState.Unprocessed;
} else {
state = localState;
}
}
break;
@ -196,9 +194,11 @@ public class XMLResponseHandler extends DefaultHandler {
state = XMLHandlerState.Presets;
} else if ("group".equals(localName)) {
this.masterDeviceId = new BoseSoundTouchConfiguration();
state = stateMap.get(localName);
} else {
if (localState == null) {
logger.debug("{}: Unhandled XML entity during {}: '{}", handler.getDeviceName(), curState,
state = stateMap.get(localName);
if (state == null) {
logger.warn("{}: Unhandled XML entity during {}: '{}", handler.getDeviceName(), curState,
localName);
state = XMLHandlerState.Unprocessed;
@ -366,16 +366,12 @@ public class XMLResponseHandler extends DefaultHandler {
if (contentItem == null) {
contentItem = new ContentItem();
}
String source = "";
String location = "";
String sourceAccount = "";
Boolean isPresetable = false;
if (attributes != null) {
source = attributes.getValue("source");
sourceAccount = attributes.getValue("sourceAccount");
location = attributes.getValue("location");
isPresetable = Boolean.parseBoolean(attributes.getValue("isPresetable"));
String source = attributes.getValue("source");
String location = attributes.getValue("location");
String sourceAccount = attributes.getValue("sourceAccount");
Boolean isPresetable = Boolean.parseBoolean(attributes.getValue("isPresetable"));
if (source != null) {
contentItem.setSource(source);

View File

@ -47,6 +47,7 @@ public class XMLResponseProcessor {
public void handleMessage(String msg) throws SAXException, IOException, ParserConfigurationException {
SAXParserFactory parserFactory = SAXParserFactory.newInstance();
parserFactory.setNamespaceAware(true);
SAXParser parser = parserFactory.newSAXParser();
XMLReader reader = parser.getXMLReader();
reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

View File

@ -306,6 +306,14 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
return commandExecutor;
}
/**
* Sets the CommandExecutor of this handler
*
*/
public void setCommandExecutor(@Nullable CommandExecutor commandExecutor) {
this.commandExecutor = commandExecutor;
}
/**
* Returns the Session this handler has opened
*

View File

@ -0,0 +1,136 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.notNull;
import java.text.MessageFormat;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.binding.bosesoundtouch.internal.handler.BoseSoundTouchHandler;
import org.openhab.binding.bosesoundtouch.internal.handler.InMemmoryContentStorage;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringListType;
import org.openhab.core.storage.Storage;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
/**
*
* @author Leo Siepel - Initial contribution
*
*/
@ExtendWith(MockitoExtension.class)
@NonNullByDefault
public class SoundTouch20Tests {
private @Mock @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
private @NonNullByDefault({}) Thing soundTouchThing;
private @NonNullByDefault({}) BoseSoundTouchHandler thingHandler;
private @NonNullByDefault({}) XMLResponseProcessor processor;
private ThingUID thingUID = new ThingUID(BoseSoundTouchBindingConstants.BINDING_ID, "soundtouch20");
private ChannelUID volumeChannelUID = new ChannelUID(thingUID, BoseSoundTouchBindingConstants.CHANNEL_VOLUME);
private ChannelUID presetChannelUID = new ChannelUID(thingUID, BoseSoundTouchBindingConstants.CHANNEL_PRESET);
private Storage<@NonNull ContentItem> storage = new InMemmoryContentStorage();
private @Mock @NonNullByDefault({}) BoseStateDescriptionOptionProvider stateDescriptionProvider;
@BeforeEach
public void initialize() {
// arrange
Configuration config = new Configuration();
config.put(BoseSoundTouchConfiguration.MAC_ADDRESS, "B0D5CC1AAAA1");
soundTouchThing = ThingBuilder.create(BoseSoundTouchBindingConstants.BST_20_THING_TYPE_UID, thingUID)
.withConfiguration(config).withChannel(ChannelBuilder.create(volumeChannelUID, "Number").build())
.withChannel(ChannelBuilder.create(presetChannelUID, "Number").build()).build();
PresetContainer container = new PresetContainer(storage);
thingHandler = new BoseSoundTouchHandler(soundTouchThing, container, stateDescriptionProvider);
processor = new XMLResponseProcessor(thingHandler);
}
private void processIncomingMessage(String mesage) {
try {
processor.handleMessage(mesage);
} catch (Exception e) {
assert false : MessageFormat.format("handleMessage throws an exception: {0} Stacktrace: {1}",
e.getMessage(), e.getStackTrace());
}
}
@Test
public void configurationPropertyUpdated() {
// arange
CommandExecutor executor = new CommandExecutor(thingHandler);
thingHandler.setCommandExecutor(executor);
String message = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><msg><header deviceID=\"B0D5CC1AAAA1\" url=\"info\" method=\"GET\"><request requestID=\"0\" msgType=\"RESPONSE\"><info type=\"new\" /></request></header><body><info deviceID=\"B0D5CC1AAAA1\"><name>livingroom</name><type>SoundTouch 20</type><margeAccountUUID>3504027</margeAccountUUID><components><component><componentCategory>SCM</componentCategory><softwareVersion>27.0.6.46330.5043500 epdbuild.trunk.hepdswbld04.2022-08-04T11:20:29</softwareVersion><serialNumber>U6148010803720048000100</serialNumber></component><component><componentCategory>PackagedProduct</componentCategory><serialNumber>069430P5227013812</serialNumber></component></components><margeURL>https://streaming.bose.com</margeURL><networkInfo type=\"SCM\"><macAddress>B0D5CC1AAAA1</macAddress><ipAddress>192.168.1.1</ipAddress></networkInfo><networkInfo type=\"SMSC\"><macAddress>5CF821E2FD76</macAddress><ipAddress>192.168.1.1</ipAddress></networkInfo><moduleType>sm2</moduleType><variant>spotty</variant><variantMode>normal</variantMode><countryCode>GB</countryCode><regionCode>GB</regionCode></info></body></msg>";
// act
processIncomingMessage(message);
// assert
assertEquals("27.0.6.46330.5043500",
soundTouchThing.getProperties().get(org.openhab.core.thing.Thing.PROPERTY_FIRMWARE_VERSION));
}
@Test
public void channelVolumeUpdated() {
// arrange
CommandExecutor executor = new CommandExecutor(thingHandler);
thingHandler.setCommandExecutor(executor);
Mockito.when(thingHandlerCallback.isChannelLinked((ChannelUID) notNull())).thenReturn(true);
thingHandler.setCallback(thingHandlerCallback);
String message = "<updates deviceID=\"B0D5CC1AAAA1\"><volumeUpdated><volume><actualvolume>27</actualvolume></volume></volumeUpdated></updates>";
// act
processIncomingMessage(message);
// assert
Mockito.verify(thingHandlerCallback).stateUpdated(eq(volumeChannelUID), eq(new PercentType("27")));
}
@Test
@Disabled
public void channelPresetUpdated() {
// arrange
CommandExecutor executor = new CommandExecutor(thingHandler);
thingHandler.setCommandExecutor(executor);
Mockito.when(thingHandlerCallback.isChannelLinked((ChannelUID) notNull())).thenReturn(true);
thingHandler.setCallback(thingHandlerCallback);
String message = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><presets><preset id=\"1\" createdOn=\"1502124154\" updatedOn=\"1644607971\"><ContentItem source=\"TUNEIN\" type=\"stationurl\" location=\"/v1/playback/station/s25077\" sourceAccount=\"\" isPresetable=\"true\"><itemName>Radio FM1</itemName><containerArt>http://cdn-profiles.tunein.com/s25077/images/logoq.jpg</containerArt></ContentItem></preset><preset id=\"2\" createdOn=\"1485893875\" updatedOn=\"1612895566\"><ContentItem source=\"STORED_MUSIC\" location=\"22$2955\" sourceAccount=\"00113254-f4eb-0011-ebf4-ebf454321100/0\" isPresetable=\"true\"><itemName>Medicine At Midnight</itemName><containerArt /></ContentItem></preset><preset id=\"3\" createdOn=\"1506167722\" updatedOn=\"1506167722\"><ContentItem source=\"STORED_MUSIC\" location=\"22$1421\" sourceAccount=\"00113254-f4eb-0011-ebf4-ebf454321100/0\" isPresetable=\"true\"><itemName>Concrete &amp; Gold</itemName><containerArt /></ContentItem></preset><preset id=\"4\" createdOn=\"1444146657\" updatedOn=\"1542740566\"><ContentItem source=\"TUNEIN\" type=\"stationurl\" location=\"/v1/playback/station/s24896\" sourceAccount=\"\" isPresetable=\"true\"><itemName>SWR3</itemName><containerArt>http://radiotime-logos.s3.amazonaws.com/s24896q.png</containerArt></ContentItem></preset><preset id=\"5\" createdOn=\"1468517184\" updatedOn=\"1542740566\"><ContentItem source=\"TUNEIN\" type=\"stationurl\" location=\"/v1/playback/station/s103302\" sourceAccount=\"\" isPresetable=\"true\"><itemName>SRF 3</itemName><containerArt>http://radiotime-logos.s3.amazonaws.com/s24862q.png</containerArt></ContentItem></preset><preset id=\"6\" createdOn=\"1481548081\" updatedOn=\"1524211387\"><ContentItem source=\"STORED_MUSIC\" location=\"22$882\" sourceAccount=\"00113254-f4eb-0011-ebf4-ebf454321100/0\" isPresetable=\"true\"><itemName>Sonic Highways</itemName><containerArt /></ContentItem></preset></presets>";
// act
processIncomingMessage(message);
// assert
// TODO: check if preset channels have changed
Mockito.verify(thingHandlerCallback).stateUpdated(eq(presetChannelUID), eq(new StringListType("27")));
}
}

View File

@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal.handler;
import java.util.Collection;
import java.util.Map;
import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bosesoundtouch.internal.ContentItem;
import org.openhab.core.storage.Storage;
/**
* @author Leo Siepel - Initial contribution
*/
@NonNullByDefault
public class InMemmoryContentStorage implements Storage<ContentItem> {
Map<String, @Nullable ContentItem> items = new TreeMap<>();
public InMemmoryContentStorage() {
}
@Override
public @Nullable ContentItem put(String key, @Nullable ContentItem value) {
return items.put(key, value);
}
@Override
public @Nullable ContentItem remove(String key) {
return items.remove(key);
}
@Override
public boolean containsKey(String key) {
return items.containsKey(key);
}
@Override
public @Nullable ContentItem get(String key) {
return items.get(key);
}
@Override
public Collection<@NonNull String> getKeys() {
return items.keySet();
}
@Override
public Collection<@Nullable ContentItem> getValues() {
return items.values();
}
}