package com.battery.device.bts; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.EOFException; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import org.junit.jupiter.api.Test; /** * 远程命名管道传输的行为:分帧按实测的 {@code \n\n}、只读白名单在发出前生效。 * 分帧用普通文件驱动 {@link NewareBtsPipeTransport#readFrame} —— 生产方法读写的就是 {@code RandomAccessFile}, * 换掉的是对端是不是管道,帧扫描这一段完全一致。 */ class NewareBtsPipeTransportTest { private static final int MAX = 1024; @Test void returnsPayloadUpToThePlainDoubleNewlineTerminator() throws Exception { assertEquals("getdevinfo_resp", frame("getdevinfo_resp\n\n")); } @Test void ignoresBytesAfterTheTerminator() throws Exception { assertEquals("first", frame("first\n\nsecond\n\n")); } @Test void keepsTerminatorPrefixThatBelongsToThePayload() throws Exception { // 正文里出现结束符的前缀时要原样补回,不能把它吃掉。 assertEquals("a\n#b", frame("a\n#b\n\n")); } @Test void doesNotTreatDocumentedHashTerminatorAsAnEnd() throws Exception { // 协议文档给的 #\r\n 现场实测无效;照它写就永远读不到帧尾,这里以"读到尾也没结束"表示这一点。 assertThrows(EOFException.class, () -> frame("#\r\n")); } @Test void rejectsPayloadBeyondTheLimit() throws Exception { assertThrows(IOException.class, () -> frameRaw("123456789\n\n", 8)); } @Test void allowsPayloadExactlyAtTheLimit() throws Exception { assertEquals("12345678", frameRaw("12345678\n\n", 8)); } @Test void rejectsEveryCommandOutsideTheReadOnlyWhitelistBeforeAnyTransportWork() { try (NewareBtsPipeTransport transport = new NewareBtsPipeTransport("203.0.113.1", "Administrator", "secret", Duration.ofSeconds(1), Duration.ofSeconds(1))) { // 控制命令一律不得发出:这里没有任何会话与管道,若白名单没生效就会变成连接失败而不是被拒绝。 for (String command : new String[] {"start", "stop", "reset", "broadcaststop", "setpause", "cancelpause", "goto", "chl_ctrl", "parallel", "light", "continue", "clearflag"}) { BtsTransportException exception = assertThrows(BtsTransportException.class, () -> transport.exchange("" + command + "")); assertEquals(BtsTransportException.Reason.READ_ONLY_REJECTED, exception.reason(), command); } } } @Test void acceptsTheReadOnlyWhitelistAndRejectsRequestsWithoutACommand() { try (NewareBtsPipeTransport transport = new NewareBtsPipeTransport("203.0.113.1", "Administrator", "secret", Duration.ofSeconds(1), Duration.ofSeconds(1))) { for (String command : new String[] {"getdevinfo", "getchlstatus", "inquire", "inquiredf", "download", "downloadStepLayer", "downloadlog", "getparallel", "getpause"}) assertEquals(command.toLowerCase(java.util.Locale.ROOT), NewareBtsReadOnlyCommands.requireAllowed("" + command + "")); BtsTransportException exception = assertThrows(BtsTransportException.class, () -> NewareBtsReadOnlyCommands.requireAllowed("没有命令")); assertEquals(BtsTransportException.Reason.READ_ONLY_REJECTED, exception.reason()); } } private static String frame(String content) throws Exception { return frameRaw(content, MAX); } private static String frameRaw(String content, int maxPayloadBytes) throws Exception { Path file = Files.createTempFile("neware-bts-frame", ".txt"); try { Files.write(file, content.getBytes(StandardCharsets.UTF_8)); try (RandomAccessFile pipe = new RandomAccessFile(file.toFile(), "r")) { return new String(NewareBtsPipeTransport.readFrame(pipe, maxPayloadBytes), StandardCharsets.UTF_8); } } finally { Files.deleteIfExists(file); } } }