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 = "getdevinfo_resp" + "true" + ""; 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 = "getdevinfo_resp" + "maybe" + ""; BtsProtocolException exception = assertThrows(BtsProtocolException.class, () -> codec.parseDeviceInfo(xml)); assertEquals(BtsProtocolException.Reason.INVALID_VALUE, exception.reason()); } @Test void parsesRealtimeCoreDynamicAndDbcMeasurements() { String xml = "inquire_resp" + "" + "" + ""; Map 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 = "inquire_resp"; RealtimeSnapshot snapshot = codec.parseSnapshots(xml, Instant.EPOCH).values().iterator().next(); assertEquals(DeviceWorkStatus.UNKNOWN, snapshot.workStatus()); } @Test void mapsFinishAsNormalCompletion() { String xml = "inquire_resp" + ""; 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 = "download_resp" + "" + "" + "" + ""; 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 = "download_resp" + "" + ""; 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 = "inquiredf_resp" + "true" + "false" + ""; List result = codec.parseHistoryAvailability(response); assertTrue(request.contains("inquiredf")); 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 = "inquire_resp"; 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("download_resp", Instant.EPOCH)); BtsProtocolException malformed = assertThrows(BtsProtocolException.class, () -> codec.parseSnapshots("inquire_resp", Instant.EPOCH)); assertEquals(BtsProtocolException.Reason.UNEXPECTED_COMMAND, wrongCommand.reason()); assertEquals(BtsProtocolException.Reason.MALFORMED_XML, malformed.reason()); } @Test void reportsMissingRequiredField() { String xml = "inquire_resp"; 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("getdevinfo_resp")); BtsProtocolException realtime = assertThrows(BtsProtocolException.class, () -> codec.parseSnapshots("inquire_resp", Instant.EPOCH)); BtsProtocolException availability = assertThrows(BtsProtocolException.class, () -> codec.parseHistoryAvailability("inquiredf_resp")); ChannelIdentity channel = new ChannelIdentity("lab-bts", 24, 1, 1, 1); BtsProtocolException history = assertThrows(BtsProtocolException.class, () -> codec.parseHistory("download_resp" + "" + "", 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 = "]>inquire_resp&xxe;"; BtsProtocolException exception = assertThrows(BtsProtocolException.class, () -> codec.parseSnapshots(xml, Instant.EPOCH)); assertFalse(exception.getMessage().isBlank()); } }