Files
fpc-comet/tests/test_init.pas
Ken Johnson f3de3a594c Session handshake helpers: cm.session + INIT codec tests
cm.session
----------
Building blocks for the Comet wire handshake (FSP-COMET-001
section 7), pure encoding / decoding -- no I/O state machine,
no CRC, no transport.  cm.driver (next commit) will compose
these with cm.frame + transport into the actual session
engine.

Records (use mb.address.TFTNAddress, not the daemon's
TCometAddress, so wire layout is consistent across the
ecosystem):

  TCometInitInfo -- pre-handshake info we send / they send
  in NPKT_INIT.  Wire encoding documented inline:

      uint32 LE  Version
      uint32 LE  Caps (COPT_* mask)
      uint32 LE  MaxBlock
      uint32 LE  WindowSize (low 16 bits)
      uint8      NumAddresses
      [Zone(2) Net(2) Node(2) Point(2)] * NumAddresses
      NUL strings: Password, SysName, SysOp, Mailer,
                   Location, Time, Phone, NodelistFlags

  Last four trailing strings are backward-compatible -- a
  reader that runs off the end of the actual payload just
  gets empty values for the missing tail fields.

  TCometNegotiated -- agreed session params after INIT
  exchange (caps = AND of both sides, block / window =
  min of both sides, remote AKAs / metadata copied).

Functions:
  CMSendBanner / CMRecvBanner -- transport-level banner
    exchange via IComTransport.  CMRecvBanner does the
    BinkP-vs-Comet first-byte detection and returns
    cbrBinkP + a 1-byte PeekBuf when the peer's first
    byte has the high bit set, so the daemon can hand off
    cleanly to its BinkP backend (the same pattern the
    Comet daemon uses today between cometses + cometbinkp).

  CMBuildInit / CMParseInit -- NPKT_INIT codec.
  CMBuildInitAck / CMParseInitAck -- NPKT_INITACK codec.
  CMNegotiate -- intersect caps + min block/window,
    preserve WeAreOriginator across the call.

cm.session has no logger or transport state of its own --
it's pure helpers that cm.driver will compose.

test_init
---------
39 checks across 5 cases:

  - Basic INIT (1 address, no trailing) round-trips.
  - Full INIT (2 addresses, all 4 trailing strings)
    round-trips byte-exact.
  - Truncated trailing tolerated (rev-1 backward compat:
    cut the wire after the Mailer NUL, re-parse, get
    empty Location/Time/Phone/NodelistFlags).
  - CMNegotiate computes AND of caps, min of block /
    window, preserves WeAreOriginator.
  - INITACK round-trip (12 bytes exact).

run_tests.sh
------------
Mirror of fpc-binkp's test driver -- builds the library
natively, then compiles + runs every test_*.pas under
tests/.  Produces the same PASS / FAIL / Results: line
format.

State: 14 cm.* units compile clean on all 7 targets
(x86_64-linux, x86_64-freebsd, i386-go32v2, i386-os2,
i386-win32, i386-linux, i386-freebsd).  60 tests across
2 test programs all green.

Next: cm.driver (the TComSession state machine that
threads cm.frame + cm.session + cm.crypto into the actual
NextStep loop) and cm.transport.tcp (UNIX BSD socket
reference impl), then the cm.xfer file-transfer state
machine.
2026-04-22 10:09:04 -07:00

212 lines
6.1 KiB
ObjectPascal

{ test_init -- INIT codec round-trip + negotiation. }
program test_init;
{$mode objfpc}{$H+}
{$modeswitch advancedrecords}
uses
Classes, SysUtils,
mb.address,
cm.types, cm.session;
var
TestsRun, TestsFailed: Integer;
procedure Check(Cond: Boolean; const Msg: string);
begin
Inc(TestsRun);
if not Cond then
begin
Inc(TestsFailed);
Writeln(' FAIL: ', Msg);
end;
end;
procedure CheckStr(const A, B, Msg: string);
begin
Check(A = B, Format('%s (got "%s", want "%s")', [Msg, A, B]));
end;
procedure TestInitRoundTripBasic;
var
Built: TBytes;
I, P: TCometInitInfo;
begin
Writeln('TestInitRoundTripBasic');
I := Default(TCometInitInfo);
I.Version := CM_PROTO_REVISION;
I.Caps := COPT_SHA256 or COPT_FREQ or COPT_NOPWD;
I.MaxBlock := CM_DEF_BLOCK_SIZE;
I.WindowSize := CM_DEF_WINDOW;
SetLength(I.Addresses, 1);
I.Addresses[0] := MakeFTNAddress(1, 218, 720, 0);
I.Password := 'secret';
I.SysName := 'Test System';
I.SysOp := 'Sysop';
I.Mailer := 'fpc-comet/0.1.0';
Built := CMBuildInit(I);
Check(Length(Built) > 17, 'built INIT > 17 bytes');
Check(CMParseInit(Built, P), 'parse round-trips OK');
Check(P.Version = I.Version, 'Version');
Check(P.Caps = I.Caps, 'Caps');
Check(P.MaxBlock = I.MaxBlock, 'MaxBlock');
Check(P.WindowSize = I.WindowSize, 'WindowSize');
Check(Length(P.Addresses) = 1, 'one address parsed');
Check(FTNAddressEqual(P.Addresses[0], I.Addresses[0]),
'address preserved');
CheckStr(P.Password, I.Password, 'Password');
CheckStr(P.SysName, I.SysName, 'SysName');
CheckStr(P.SysOp, I.SysOp, 'SysOp');
CheckStr(P.Mailer, I.Mailer, 'Mailer');
CheckStr(P.Location, '', 'Location empty');
CheckStr(P.Phone, '', 'Phone empty');
end;
procedure TestInitWithTrailingStrings;
var
Built: TBytes;
I, P: TCometInitInfo;
begin
Writeln('TestInitWithTrailingStrings');
I := Default(TCometInitInfo);
I.Version := CM_PROTO_REVISION;
I.Caps := COPT_ALL;
I.MaxBlock := CM_MAX_BLOCK_SIZE;
I.WindowSize := CM_MAX_WINDOW;
SetLength(I.Addresses, 2);
I.Addresses[0] := MakeFTNAddress(1, 218, 720, 0);
I.Addresses[1] := MakeFTNAddress(1, 218, 720, 5);
I.Password := 'p';
I.SysName := 'Multi-AKA Node';
I.SysOp := 'Operator';
I.Mailer := 'fpc-comet/test';
I.Location := 'Reno, NV';
I.Time := 'Wed, 22 Apr 2026 12:34:56 +0000';
I.Phone := '775-555-0100';
I.NodelistFlags := 'CM,V90C,X75,IBN,XX,U,T9';
Built := CMBuildInit(I);
Check(CMParseInit(Built, P), 'parse round-trips');
Check(Length(P.Addresses) = 2, 'two addresses');
Check(FTNAddressEqual(P.Addresses[0], I.Addresses[0]), 'AKA 0');
Check(FTNAddressEqual(P.Addresses[1], I.Addresses[1]), 'AKA 1');
CheckStr(P.Location, I.Location, 'Location');
CheckStr(P.Time, I.Time, 'Time');
CheckStr(P.Phone, I.Phone, 'Phone');
CheckStr(P.NodelistFlags, I.NodelistFlags, 'NodelistFlags');
end;
procedure TestInitTruncatedTrailingTolerated;
var
Full, Truncated: TBytes;
I, P: TCometInitInfo;
CutAt, J: Integer;
begin
Writeln('TestInitTruncatedTrailingTolerated');
I := Default(TCometInitInfo);
I.Version := CM_PROTO_REVISION;
I.Caps := COPT_NOPWD;
I.MaxBlock := CM_DEF_BLOCK_SIZE;
I.WindowSize := CM_DEF_WINDOW;
SetLength(I.Addresses, 1);
I.Addresses[0] := MakeFTNAddress(1, 213, 725, 0);
I.Password := 'p'; I.SysName := 's'; I.SysOp := 'o'; I.Mailer := 'm';
I.Location := 'L'; I.Phone := 'PH';
Full := CMBuildInit(I);
{ Truncate after Mailer NUL. Walk the payload counting
the first 4 NUL terminators (Password, SysName, SysOp,
Mailer) and cut there. }
CutAt := 17 + 8; { fixed + 1 address }
for J := 0 to 3 do
begin
while (CutAt < Length(Full)) and (Full[CutAt] <> 0) do
Inc(CutAt);
Inc(CutAt); { past the NUL }
end;
SetLength(Truncated, CutAt);
Move(Full[0], Truncated[0], CutAt);
Check(CMParseInit(Truncated, P), 'truncated payload parses');
CheckStr(P.Password, 'p', 'rev-1 fields preserved');
CheckStr(P.SysName, 's', 'rev-1 SysName');
CheckStr(P.Mailer, 'm', 'rev-1 Mailer');
CheckStr(P.Location, '', 'trailing Location empty');
CheckStr(P.Phone, '', 'trailing Phone empty');
end;
procedure TestNegotiation;
var
A, B: TCometInitInfo;
Sess: TCometNegotiated;
begin
Writeln('TestNegotiation');
A := Default(TCometInitInfo);
A.Caps := COPT_SHA256 or COPT_FREQ or COPT_LST or COPT_NOPWD;
A.MaxBlock := 16384;
A.WindowSize := 8;
B := Default(TCometInitInfo);
B.Caps := COPT_SHA256 or COPT_LST or COPT_CRYPT;
B.MaxBlock := 8192;
B.WindowSize := 4;
SetLength(B.Addresses, 1);
B.Addresses[0] := MakeFTNAddress(1, 213, 700, 0);
B.SysName := 'Other';
B.Mailer := 'other';
Sess := Default(TCometNegotiated);
Sess.WeAreOriginator := True;
CMNegotiate(A, B, Sess);
Check(Sess.SharedCaps = (COPT_SHA256 or COPT_LST),
'caps = AND of both sides');
Check(Sess.MaxBlock = 8192, 'MaxBlock = min(A,B)');
Check(Sess.WindowSize = 4, 'WindowSize = min(A,B)');
Check(Length(Sess.RemoteAddrs) = 1, 'remote AKAs preserved');
Check(Sess.WeAreOriginator, 'WeAreOriginator preserved across negotiate');
CheckStr(Sess.RemoteSysName, 'Other', 'RemoteSysName preserved');
end;
procedure TestInitAckRoundTrip;
var
Sess, P: TCometNegotiated;
Built: TBytes;
Caps, Block: LongWord;
Win: Word;
begin
Writeln('TestInitAckRoundTrip');
Sess.SharedCaps := COPT_SHA256 or COPT_FREQ;
Sess.MaxBlock := 32768;
Sess.WindowSize := 16;
Built := CMBuildInitAck(Sess);
Check(Length(Built) = 12, 'INITACK is 12 bytes');
Check(CMParseInitAck(Built, Caps, Block, Win), 'parse INITACK');
Check(Caps = Sess.SharedCaps, 'SharedCaps');
Check(Block = Sess.MaxBlock, 'MaxBlock');
Check(Win = Sess.WindowSize, 'WindowSize');
end;
begin
TestsRun := 0;
TestsFailed := 0;
TestInitRoundTripBasic;
TestInitWithTrailingStrings;
TestInitTruncatedTrailingTolerated;
TestNegotiation;
TestInitAckRoundTrip;
Writeln;
Writeln(Format('%d tests run, %d failed', [TestsRun, TestsFailed]));
if TestsFailed > 0 then Halt(1);
end.