Files
Nonemm/tests/Nonemm.Spotting.Tests/TelnetStreamTests.cs
ericek111 2b5ab54e94 Break cluster lines on a bare carriage return
N1MM splits the telnet stream on CR, not LF, which says some nodes send CR with
no LF after it. Against those the assembler was handing the whole session back
as one unfinished line.

Also adds "user:" to the login prompts, which N1MM matches and we did not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 15:53:23 +00:00

138 lines
4.1 KiB
C#

using System.Text;
using Nonemm.Spotting.Telnet;
namespace Nonemm.Spotting.Tests;
public class TelnetStreamTests
{
private const byte Iac = 255;
private const byte SubnegotiationEnd = 240;
private const byte SubnegotiationBegin = 250;
private const byte Will = 251;
private const byte Wont = 252;
private const byte Do = 253;
private const byte Dont = 254;
private const byte Echo = 1;
private const byte SuppressGoAhead = 3;
private const byte TerminalType = 24;
private static byte[] Text(string text) => Encoding.Latin1.GetBytes(text);
private static async Task<(string Read, IReadOnlyList<byte> Written)> Run(params byte[][] blocks)
{
FakeTelnetStream fake = new(blocks);
TelnetStream telnet = new(fake);
StringBuilder read = new();
while (await telnet.ReadAsync() is { } text)
{
read.Append(text);
}
return (read.ToString(), fake.Written);
}
[Fact]
public async Task PlainTextComesBackUnchanged()
{
(string read, _) = await Run(Text("DX de W3LPL: 14025.0 JA1XYZ\r\n"));
Assert.Equal("DX de W3LPL: 14025.0 JA1XYZ\r\n", read);
}
/// N1MM never answers the negotiation. It drops bytes 251, 252 and 255 from
/// what it displays but lets 253 and 254 through, so a node that opens with
/// IAC DO TERMINAL-TYPE puts two stray characters in its packet window and
/// leaves the node waiting for an answer that never comes. We answer.
[Fact]
public async Task TheNegotiationIsAnsweredAndKeptOutOfTheText()
{
(string read, IReadOnlyList<byte> written) = await Run(
[Iac, Do, TerminalType, .. Text("login: ")]);
Assert.Equal("login: ", read);
Assert.Equal([Iac, Wont, TerminalType], written);
}
[Fact]
public async Task AnOptionWeSupportIsAgreedTo()
{
(_, IReadOnlyList<byte> written) = await Run([Iac, Will, SuppressGoAhead]);
Assert.Equal([Iac, Do, SuppressGoAhead], written);
}
[Fact]
public async Task AnOptionWeDoNotSupportIsRefused()
{
(_, IReadOnlyList<byte> written) = await Run([Iac, Will, TerminalType]);
Assert.Equal([Iac, Dont, TerminalType], written);
}
/// Answering an option that is already on would have the two ends replying
/// to each other for as long as the connection lasts.
[Fact]
public async Task TheSameOptionIsNotAgreedToTwice()
{
(_, IReadOnlyList<byte> written) = await Run(
[Iac, Will, Echo],
[Iac, Will, Echo]);
Assert.Equal([Iac, Do, Echo], written);
}
[Fact]
public async Task AnUnwantedOptionIsNotRefusedAgainAfterItIsOff()
{
(_, IReadOnlyList<byte> written) = await Run([Iac, Wont, Echo]);
Assert.Empty(written);
}
[Fact]
public async Task ASubnegotiationIsSwallowedWhole()
{
(string read, _) = await Run(
[.. Text("a"), Iac, SubnegotiationBegin, TerminalType, 1, 2, 3, Iac, SubnegotiationEnd, .. Text("b")]);
Assert.Equal("ab", read);
}
/// A command can arrive split over two reads, because a socket hands over
/// whatever has turned up rather than whole messages.
[Fact]
public async Task ACommandSplitAcrossTwoBlocksIsStillUnderstood()
{
(string read, IReadOnlyList<byte> written) = await Run(
[.. Text("ab"), Iac],
[Do, TerminalType, .. Text("cd")]);
Assert.Equal("abcd", read);
Assert.Equal([Iac, Wont, TerminalType], written);
}
[Fact]
public async Task ADoubledIacIsOneByteOfText()
{
(string read, _) = await Run([.. Text("a"), Iac, Iac, .. Text("b")]);
Assert.Equal("aÿb", read);
}
[Fact]
public async Task TheNullOfABareCarriageReturnIsDropped()
{
(string read, _) = await Run([.. Text("a\r"), 0, .. Text("b")]);
Assert.Equal("a\rb", read);
}
[Fact]
public async Task ALineGoesOutWithTheEndingTelnetAsksFor()
{
FakeTelnetStream fake = new();
await new TelnetStream(fake).WriteLineAsync("dx 14025.0 JA1XYZ");
Assert.Equal(Text("dx 14025.0 JA1XYZ\r\n"), fake.Written);
}
}