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 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 written) = await Run( [Iac, Do, TerminalType, .. Text("login: ")]); Assert.Equal("login: ", read); Assert.Equal([Iac, Wont, TerminalType], written); } [Fact] public async Task AnOptionWeSupportIsAgreedTo() { (_, IReadOnlyList written) = await Run([Iac, Will, SuppressGoAhead]); Assert.Equal([Iac, Do, SuppressGoAhead], written); } [Fact] public async Task AnOptionWeDoNotSupportIsRefused() { (_, IReadOnlyList 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 written) = await Run( [Iac, Will, Echo], [Iac, Will, Echo]); Assert.Equal([Iac, Do, Echo], written); } [Fact] public async Task AnUnwantedOptionIsNotRefusedAgainAfterItIsOff() { (_, IReadOnlyList 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 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); } }