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("<bts><cmd>getdevinfo_resp</cmd></bts>", frame("<bts><cmd>getdevinfo_resp</cmd></bts>\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("<bts/>#\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("<bts><cmd>" + command + "</cmd></bts>"));
|
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("<bts><cmd>" + command + "</cmd></bts>"));
|
|
BtsTransportException exception = assertThrows(BtsTransportException.class,
|
() -> NewareBtsReadOnlyCommands.requireAllowed("<bts><note>没有命令</note></bts>"));
|
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);
|
}
|
}
|
}
|