package com.ruoyi.device.bts;
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import java.math.BigDecimal;
|
import java.time.Instant;
|
import java.time.ZoneId;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Set;
|
import org.junit.jupiter.api.Test;
|
import com.ruoyi.device.model.ChannelIdentity;
|
import com.ruoyi.device.model.DeviceWorkStatus;
|
import com.ruoyi.device.model.HistoryPage;
|
import com.ruoyi.device.model.HistoryAvailability;
|
import com.ruoyi.device.model.RealtimeSnapshot;
|
|
class BtsXmlCodecTest
|
{
|
private final BtsXmlCodec codec = new BtsXmlCodec("lab-bts", ZoneId.of("Asia/Hong_Kong"));
|
|
@Test
|
void parsesDeviceDiscoveryAndChannelIdentity()
|
{
|
String xml = "<bts version=\"1.0\"><cmd>getdevinfo_resp</cmd><middle count=\"1\">"
|
+ "<channel ip=\"192.168.1.35\" devtype=\"22\" devid=\"1\" subdevid=\"2\" Channelid=\"3\">true</channel>"
|
+ "</middle></bts>";
|
|
var channels = codec.parseDeviceInfo(xml);
|
|
assertEquals(1, channels.size());
|
assertEquals(new ChannelIdentity("lab-bts", 22, 1, 2, 3), channels.get(0).identity());
|
assertEquals("192.168.1.35", channels.get(0).serverIp());
|
}
|
|
@Test
|
void rejectsInvalidDiscoveryAvailability()
|
{
|
String xml = "<bts><cmd>getdevinfo_resp</cmd><middle count=\"1\">"
|
+ "<channel ip=\"192.168.1.35\" devtype=\"22\" devid=\"1\" subdevid=\"2\" Channelid=\"3\">maybe</channel>"
|
+ "</middle></bts>";
|
|
BtsProtocolException exception = assertThrows(BtsProtocolException.class,
|
() -> codec.parseDeviceInfo(xml));
|
|
assertEquals(BtsProtocolException.Reason.INVALID_VALUE, exception.reason());
|
}
|
|
@Test
|
void parsesRealtimeCoreDynamicAndDbcMeasurements()
|
{
|
String xml = "<bts version=\"1.0\"><cmd>inquire_resp</cmd><list count=\"1\">"
|
+ "<inquire dev=\"22-3-2-7-0\" cycle_id=\"2\" step_id=\"4\" step_type=\"cc\" workstatus=\"working\" "
|
+ "current=\"1.2\" voltage=\"3.5\" capacity=\"0.1\" energy=\"0.35\" T1=\"23.4\" log_code=\"0\">"
|
+ "<DBC message_name=\"Frame1\" signal_name=\"Soc\" value=\"88.5\" unit=\"%\"/></inquire>"
|
+ "</list></bts>";
|
|
Map<ChannelIdentity, RealtimeSnapshot> result = codec.parseSnapshots(xml, Instant.parse("2026-08-25T10:00:00Z"));
|
RealtimeSnapshot snapshot = result.values().iterator().next();
|
|
assertEquals(DeviceWorkStatus.WORKING, snapshot.workStatus());
|
assertEquals(new BigDecimal("1.2"), snapshot.currentA());
|
assertEquals(new BigDecimal("23.4"), snapshot.measurements().get("T1").value());
|
assertEquals("%", snapshot.measurements().get("DBC:Frame1:Soc").unit());
|
}
|
|
@Test
|
void mapsUnknownWorkStatusWithoutDiscardingSnapshot()
|
{
|
String xml = "<bts><cmd>inquire_resp</cmd><list count=\"1\"><inquire dev=\"22-1-1-1-0\" workstatus=\"futureState\"/></list></bts>";
|
|
RealtimeSnapshot snapshot = codec.parseSnapshots(xml, Instant.EPOCH).values().iterator().next();
|
|
assertEquals(DeviceWorkStatus.UNKNOWN, snapshot.workStatus());
|
}
|
|
@Test
|
void mapsFinishAsNormalCompletion()
|
{
|
String xml = "<bts><cmd>inquire_resp</cmd><list count=\"1\">"
|
+ "<inquire dev=\"22-1-1-1-0\" workstatus=\"finish\"/></list></bts>";
|
|
RealtimeSnapshot snapshot = codec.parseSnapshots(xml, Instant.EPOCH).values().iterator().next();
|
|
assertEquals(DeviceWorkStatus.FINISHED, snapshot.workStatus());
|
}
|
|
@Test
|
void parsesHistoryPageAndHongKongDeviceTime()
|
{
|
ChannelIdentity channel = new ChannelIdentity("lab-bts", 24, 1, 1, 1);
|
String xml = "<bts><cmd>download_resp</cmd>"
|
+ "<download devtype=\"24\" devid=\"1\" subdevid=\"1\" chlid=\"1\" testid=\"42\"/>"
|
+ "<list count=\"1\">"
|
+ "<data seqid=\"7\" stepid=\"1\" cycleid=\"2\" steptype=\"cc\" testtime=\"1000\" "
|
+ "atime=\"2026-08-25 18.00.00\" volt=\"3.5\" curr=\"1.2\" cap=\"0.1\" eng=\"0.35\" T1=\"25\"/>"
|
+ "</list></bts>";
|
|
HistoryPage page = codec.parseHistory(xml, channel, "42", 7);
|
|
assertEquals("42", page.btsTestId());
|
assertEquals(8, page.nextPosition());
|
assertEquals(Instant.parse("2026-08-25T10:00:00Z"), page.samples().get(0).deviceTime());
|
}
|
|
@Test
|
void rejectsHistoryResponseForDifferentChannelOrTest()
|
{
|
ChannelIdentity requested = new ChannelIdentity("lab-bts", 24, 1, 1, 1);
|
String xml = "<bts><cmd>download_resp</cmd>"
|
+ "<download devtype=\"24\" devid=\"1\" subdevid=\"1\" chlid=\"2\" testid=\"99\"/>"
|
+ "<list count=\"0\"/></bts>";
|
|
BtsProtocolException exception = assertThrows(BtsProtocolException.class,
|
() -> codec.parseHistory(xml, requested, "42", 1));
|
|
assertEquals(BtsProtocolException.Reason.IDENTITY_MISMATCH, exception.reason());
|
}
|
|
@Test
|
void buildsAndParsesHistoryAvailabilityForMultipleChannels()
|
{
|
ChannelIdentity first = new ChannelIdentity("lab-bts", 22, 3, 2, 8);
|
ChannelIdentity second = new ChannelIdentity("lab-bts", 22, 3, 1, 6);
|
|
String request = codec.inquireHistoryRequest(Set.of(first, second));
|
String response = "<bts><cmd>inquiredf_resp</cmd><list count=\"2\">"
|
+ "<chl devtype=\"22\" devid=\"3\" subdevid=\"2\" chlid=\"8\" testid=\"11\" count=\"10\">true</chl>"
|
+ "<chl devtype=\"22\" devid=\"3\" subdevid=\"1\" chlid=\"6\" testid=\"10\" count=\"11\">false</chl>"
|
+ "</list></bts>";
|
|
List<HistoryAvailability> result = codec.parseHistoryAvailability(response);
|
|
assertTrue(request.contains("<cmd>inquiredf</cmd>"));
|
assertTrue(request.contains("devtype=\"22\" devid=\"3\" subdevid=\"2\" chlid=\"8\" testid=\"0\""));
|
assertTrue(request.contains("devtype=\"22\" devid=\"3\" subdevid=\"1\" chlid=\"6\" testid=\"0\""));
|
assertEquals(new HistoryAvailability(first, "11", 10, true), result.get(0));
|
assertEquals(new HistoryAvailability(second, "10", 11, false), result.get(1));
|
}
|
|
@Test
|
void rejectsIllegalNumericData()
|
{
|
String xml = "<bts><cmd>inquire_resp</cmd><list count=\"1\"><inquire dev=\"22-1-1-1-0\" current=\"bad\"/></list></bts>";
|
|
BtsProtocolException exception = assertThrows(BtsProtocolException.class,
|
() -> codec.parseSnapshots(xml, Instant.EPOCH));
|
|
assertEquals(BtsProtocolException.Reason.INVALID_VALUE, exception.reason());
|
}
|
|
@Test
|
void classifiesWrongCommandAndMalformedXml()
|
{
|
BtsProtocolException wrongCommand = assertThrows(BtsProtocolException.class,
|
() -> codec.parseSnapshots("<bts><cmd>download_resp</cmd></bts>", Instant.EPOCH));
|
BtsProtocolException malformed = assertThrows(BtsProtocolException.class,
|
() -> codec.parseSnapshots("<bts><cmd>inquire_resp</cmd>", Instant.EPOCH));
|
|
assertEquals(BtsProtocolException.Reason.UNEXPECTED_COMMAND, wrongCommand.reason());
|
assertEquals(BtsProtocolException.Reason.MALFORMED_XML, malformed.reason());
|
}
|
|
@Test
|
void reportsMissingRequiredField()
|
{
|
String xml = "<bts><cmd>inquire_resp</cmd><list count=\"1\"><inquire workstatus=\"working\"/></list></bts>";
|
|
BtsProtocolException exception = assertThrows(BtsProtocolException.class,
|
() -> codec.parseSnapshots(xml, Instant.EPOCH));
|
|
assertEquals(BtsProtocolException.Reason.MISSING_FIELD, exception.reason());
|
}
|
|
@Test
|
void rejectsResponsesMissingRequiredContainers()
|
{
|
BtsProtocolException discovery = assertThrows(BtsProtocolException.class,
|
() -> codec.parseDeviceInfo("<bts><cmd>getdevinfo_resp</cmd></bts>"));
|
BtsProtocolException realtime = assertThrows(BtsProtocolException.class,
|
() -> codec.parseSnapshots("<bts><cmd>inquire_resp</cmd></bts>", Instant.EPOCH));
|
BtsProtocolException availability = assertThrows(BtsProtocolException.class,
|
() -> codec.parseHistoryAvailability("<bts><cmd>inquiredf_resp</cmd></bts>"));
|
ChannelIdentity channel = new ChannelIdentity("lab-bts", 24, 1, 1, 1);
|
BtsProtocolException history = assertThrows(BtsProtocolException.class,
|
() -> codec.parseHistory("<bts><cmd>download_resp</cmd>"
|
+ "<download devtype=\"24\" devid=\"1\" subdevid=\"1\" chlid=\"1\" testid=\"42\"/>"
|
+ "</bts>", channel, "42", 1));
|
|
assertEquals(BtsProtocolException.Reason.MISSING_FIELD, discovery.reason());
|
assertEquals(BtsProtocolException.Reason.MISSING_FIELD, realtime.reason());
|
assertEquals(BtsProtocolException.Reason.MISSING_FIELD, availability.reason());
|
assertEquals(BtsProtocolException.Reason.MISSING_FIELD, history.reason());
|
}
|
|
@Test
|
void rejectsDoctypeAndExternalEntities()
|
{
|
String xml = "<!DOCTYPE bts [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]><bts><cmd>inquire_resp</cmd>&xxe;</bts>";
|
|
BtsProtocolException exception = assertThrows(BtsProtocolException.class,
|
() -> codec.parseSnapshots(xml, Instant.EPOCH));
|
assertFalse(exception.getMessage().isBlank());
|
}
|
}
|