Files
fpc-msgbase/tests/test_consumer_round1.pas
Ken Johnson a541085a4b 0.7.0: extract TFTNAddress into leaf mb.address unit
Ask 1 from fpc-binkp consumer thread: non-storage libraries
(fpc-ftn-transport, fpc-binkp, future fpc-comet-proto / fpc-emsi,
SQL-backed messaging like Fastway) only need TFTNAddress, not the
full 1041-line mb.types.  Extract to src/mb.address.pas (~90 lines,
only SysUtils) so they can cp a single file into their project.

mb.types continues to uses mb.address so existing callers see the
type transitively -- BUT FPC does not propagate record-field access
through re-export, so consumers that touch TFTNAddress.Zone/Net/
Node/Point directly must add mb.address to their own uses clause.
All 7 in-tree .uni adapters, 2 examples, 5 test harnesses updated.

No behavioural change.  Full suite passes, multi-target build
green (x86_64-linux, i386-{linux,freebsd,win32,os2,go32v2}).
2026-04-21 13:22:53 -07:00

947 lines
33 KiB
ObjectPascal

{
test_consumer_round1.pas - regression tests for 0.3.5 changes
driven by integration feedback from NetReader/Fimail.
Covers:
1. INTL / FMPT / TOPT FTSC kludge round-trip on JAM, MSG, PKT,
and Squish (the four formats whose capability lists include
them).
2. Unified `kludge.<name>` namespace -- unknown FTSC kludges
land under kludge.<lowername> regardless of which backend
stored them, so consumers don't need to switch on format
to find passthrough kludges.
3. msg.Attributes['area'] auto-populated from base.AreaTag on
Read when caller passed the tag to MessageBaseOpen.
4. Attributes.GetList / SetList / AppendListItem multi-line
accessors (so consumers don't roll their own #13 split).
Items 5+ (PKT-related: explicit raise on Write, CreateFromStream
/ CreateNewToStream stream targets) moved to fpc-ftn-transport's
test_pkt_writer.pas in fpc-msgbase 0.4.0 along with the PKT
code itself.
}
program test_consumer_round1;
{$mode objfpc}{$H+}
{$modeswitch advancedrecords}
uses
Classes, SysUtils,
testutil,
mb.address, mb.types, mb.events, mb.api, mb.kludge,
mb.fmt.jam, mb.fmt.jam.uni,
mb.fmt.squish, mb.fmt.squish.uni,
mb.fmt.msg, mb.fmt.msg.uni,
mb.fmt.hudson, mb.fmt.hudson.uni,
mb.fmt.goldbase, mb.fmt.goldbase.uni,
mb.fmt.pcboard, mb.fmt.pcboard.uni,
mb.fmt.ezycom, mb.fmt.ezycom.uni,
mb.fmt.wildcat, mb.fmt.wildcat.uni;
const
SCRATCH = '/tmp/ma_consumer_r1';
procedure CleanDir(const APath: string);
var
sr: TSearchRec;
begin
if not DirectoryExists(APath) then exit;
if FindFirst(APath + '/*', faAnyFile, sr) = 0 then
try
repeat
if (sr.Attr and faDirectory) = 0 then
DeleteFile(APath + '/' + sr.Name);
until FindNext(sr) <> 0;
finally
FindClose(sr);
end;
end;
function MakeIntlMsg: TUniMessage;
begin
Result.Attributes.Clear;
Result.Attributes.SetValue('from', 'Cross-Zone Sender');
Result.Attributes.SetValue('to', 'Cross-Zone Recipient');
Result.Attributes.SetValue('subject', 'INTL kludge test');
Result.Attributes.SetDate('date.written', Now);
Result.Attributes.SetAddr('addr.orig', MakeFTNAddress(2, 100, 200, 0));
Result.Attributes.SetAddr('addr.dest', MakeFTNAddress(1, 100, 300, 1));
Result.Attributes.SetValue('intl', '1:100/300 2:100/200');
Result.Attributes.SetValue('fmpt', '0');
Result.Attributes.SetValue('topt', '1');
Result.Attributes.SetValue('msgid', '2:100/200 deadbeef');
Result.Attributes.SetValue('kludge.xfoo', 'unknown-passthrough');
Result.Body := 'Cross-zone netmail body.';
end;
procedure TestIntlRoundTrip(AFormat: TMsgBaseFormat;
const APath, AName: string);
var
base: TMessageBase;
wmsg, rmsg: TUniMessage;
begin
TestBegin(AName);
ForceDirectories(ExtractFilePath(APath));
CleanDir(ExtractFilePath(APath));
wmsg := MakeIntlMsg;
base := MessageBaseOpen(AFormat, APath, momCreate);
try
AssertTrue('Open create', base.Open);
AssertTrue('Write', base.WriteMessage(wmsg));
finally
base.Close;
base.Free;
end;
base := MessageBaseOpen(AFormat, APath, momReadOnly);
try
AssertTrue('Open read', base.Open);
AssertTrue('Read[0]', base.ReadMessage(0, rmsg));
AssertEquals('intl preserved', '1:100/300 2:100/200',
rmsg.Attributes.Get('intl'));
AssertEquals('fmpt preserved', '0', rmsg.Attributes.Get('fmpt'));
AssertEquals('topt preserved', '1', rmsg.Attributes.Get('topt'));
AssertEquals('msgid preserved', '2:100/200 deadbeef',
rmsg.Attributes.Get('msgid'));
AssertEquals('unknown kludge preserved as kludge.xfoo',
'unknown-passthrough',
rmsg.Attributes.Get('kludge.xfoo'));
finally
base.Close;
base.Free;
end;
TestOK;
end;
procedure TestAreaAutoPop;
var
base: TMessageBase;
msg: TUniMessage;
begin
TestBegin('AreaTag auto-populates msg.Attributes[area] on Read');
ForceDirectories(SCRATCH + '/area');
CleanDir(SCRATCH + '/area');
{ Seed JAM with one message. Use a stable path and set
AreaTag via the property setter so we can flip it on/off
between sessions without changing path resolution. }
base := MessageBaseOpen(mbfJam, SCRATCH + '/area/echo', momCreate);
try
AssertTrue('Open create', base.Open);
msg.Attributes.Clear;
msg.Attributes.SetValue('from', 'A');
msg.Attributes.SetValue('to', 'B');
msg.Attributes.SetValue('subject', 'hi');
msg.Body := 'body';
AssertTrue('Write', base.WriteMessage(msg));
finally
base.Close;
base.Free;
end;
{ Reopen with AreaTag set via property: read should auto-fill area. }
base := MessageBaseOpen(mbfJam, SCRATCH + '/area/echo', momReadOnly);
try
base.AreaTag := 'TEST.AREA';
AssertTrue('Open ro', base.Open);
AssertTrue('Read[0]', base.ReadMessage(0, msg));
AssertEquals('area auto-populated', 'TEST.AREA',
msg.Attributes.Get('area'));
finally
base.Close;
base.Free;
end;
{ Reopen with no AreaTag: read should leave area empty. }
base := MessageBaseOpen(mbfJam, SCRATCH + '/area/echo', momReadOnly);
try
AssertTrue('Open ro no tag', base.Open);
AssertTrue('Read[0] no tag', base.ReadMessage(0, msg));
AssertEquals('area empty when no AreaTag', '',
msg.Attributes.Get('area'));
finally
base.Close;
base.Free;
end;
TestOK;
end;
procedure TestGetSetListAccessors;
var
attrs: TMsgAttributes;
list: TStringDynArray;
begin
TestBegin('Attributes.GetList / SetList / AppendListItem');
attrs.Clear;
{ GetList on empty key -> empty array. }
list := attrs.GetList('seen-by');
AssertEquals('empty list length', 0, Length(list));
{ SetList -> stored joined with #13. }
SetLength(list, 3);
list[0] := '1/100 200 300';
list[1] := '1/200 100';
list[2] := '2/50 60';
attrs.SetList('seen-by', list);
AssertEquals('joined storage matches',
'1/100 200 300'#13'1/200 100'#13'2/50 60',
attrs.Get('seen-by'));
{ GetList round-trips. }
list := attrs.GetList('seen-by');
AssertEquals('list length', 3, Length(list));
AssertEquals('list[0]', '1/100 200 300', list[0]);
AssertEquals('list[1]', '1/200 100', list[1]);
AssertEquals('list[2]', '2/50 60', list[2]);
{ AppendListItem grows. }
attrs.AppendListItem('seen-by', '3/777');
list := attrs.GetList('seen-by');
AssertEquals('appended length', 4, Length(list));
AssertEquals('appended item', '3/777', list[3]);
{ Empty SetList removes the key. }
SetLength(list, 0);
attrs.SetList('seen-by', list);
AssertFalse('empty SetList removed key', attrs.Has('seen-by'));
TestOK;
end;
{ TestPktWriteRaisesExplicitly + TestPktCreateFromStream moved
to fpc-ftn-transport's tests/test_pkt_writer.pas along with
the PKT code itself in fpc-msgbase 0.4.0. }
procedure TestSyncWriteable;
{ NR durability gap (2026-04-18): Sync should be callable on
every writable backend without crashing. Doesn't verify the
syscall count -- just exercises the path. }
var
base: TMessageBase;
msg: TUniMessage;
formats: array[0..1] of TMsgBaseFormat = (mbfJam, mbfSquish);
paths: array[0..1] of AnsiString = ('/sync/jam/echo', '/sync/sq/sq');
raised: boolean;
i: integer;
begin
TestBegin('Sync: callable on writable backends; raises after Close');
for i := 0 to High(formats) do begin
ForceDirectories(SCRATCH + '/sync');
base := MessageBaseOpen(formats[i], SCRATCH + paths[i], momCreate);
try
AssertTrue('open ' + paths[i], base.Open);
msg.Attributes.Clear;
msg.Attributes.SetValue('from', 'A');
msg.Attributes.SetValue('to', 'B');
msg.Attributes.SetValue('subject', 'sync-check');
msg.Body := 'body';
AssertTrue('write', base.WriteMessage(msg));
base.Sync; { live: must not raise }
base.Close;
raised := false;
try base.Sync;
except on E: Exception do raised := true; end;
AssertTrue('Sync after Close raises', raised);
finally
base.Free;
end;
end;
TestOK;
end;
procedure TestSquishReplyChain;
var
base: TMessageBase;
wmsg, rmsg: TUniMessage;
rest: TStringDynArray;
begin
TestBegin('Squish reply-chain round-trip (reply1st + replynext list)');
CleanDir(SCRATCH + '/replies');
ForceDirectories(SCRATCH + '/replies');
base := MessageBaseOpen(mbfSquish, SCRATCH + '/replies/rc', momCreate);
try
AssertTrue('open', base.Open);
wmsg.Attributes.Clear;
wmsg.Attributes.SetValue('from', 'Parent');
wmsg.Attributes.SetValue('to', 'All');
wmsg.Attributes.SetValue('subject', 'reply-chain test');
wmsg.Attributes.SetDate('date.written', Now);
wmsg.Attributes.SetInt('squish.replyto', 0);
wmsg.Attributes.SetInt('squish.reply1st', 101);
wmsg.Attributes.SetList('squish.replynext',
TStringDynArray.Create('102', '103', '104'));
wmsg.Body := 'parent message';
AssertTrue('write', base.WriteMessage(wmsg));
base.Close;
AssertTrue('reopen', base.Open);
AssertTrue('read', base.ReadMessage(0, rmsg));
AssertEquals('reply1st', 101, rmsg.Attributes.GetInt('squish.reply1st', -1));
rest := rmsg.Attributes.GetList('squish.replynext');
AssertEquals('replynext count', 3, Length(rest));
AssertEquals('replynext[0]', '102', rest[0]);
AssertEquals('replynext[1]', '103', rest[1]);
AssertEquals('replynext[2]', '104', rest[2]);
finally
base.Free;
end;
TestOK;
end;
{ Write a Squish message via the low-level API with SEEN-BY / PATH
placed in the body epilogue (HPT's placement), then reopen via the
unified API and assert the attr bag was populated from the body
sweep. Pins the v0.5.4 interop fix against HPT-written Squish. }
procedure TestSquishHptBodyEpilogue;
var
native: TSquishBase;
smsg: TSquishMessage;
base: TMessageBase;
umsg: TUniMessage;
bp: string;
begin
TestBegin('Squish: body-epilogue SEEN-BY/PATH (HPT-style) migrates to attrs on Read');
CleanDir(SCRATCH + '/hpt');
ForceDirectories(SCRATCH + '/hpt');
bp := SCRATCH + '/hpt/rc';
{ ---- Init the base via the uni API (creates .sqd / .sqi with a
valid header), then drop the HPT-style message via the
native API so we can hand-build s.Body with trail-control
lines baked in at the tail. ---- }
base := MessageBaseOpen(mbfSquish, bp, momCreate);
try
AssertTrue('init open', base.Open);
finally
base.Free;
end;
native := TSquishBase.Create(bp);
try
AssertTrue('native open', native.OpenReadWrite);
FillChar(smsg, SizeOf(smsg), 0);
smsg.WhoFrom := 'Sysop';
smsg.WhoTo := 'All';
smsg.Subject := 'HPT-style layout';
smsg.AzDate := '19 Apr 26 12:00:00';
smsg.CtrlInfo := #1 + 'MSGID: 20:100/10 deadbeef'; { only MSGID in CtrlInfo }
smsg.Body := 'Body text.' + #13 +
'--- tosser' + #13 +
' * Origin: Node (20:100/10)' + #13 +
'SEEN-BY: 100/1 10' + #13 +
#1 + 'PATH: 100/10';
AssertTrue('native write', native.WriteMessage(smsg));
finally
native.Free;
end;
{ ---- Reopen via the unified API, verify attrs are populated ---- }
base := MessageBaseOpen(mbfSquish, bp, momReadOnly);
try
AssertTrue('open', base.Open);
AssertTrue('read', base.ReadMessage(0, umsg));
AssertEquals('msgid from CtrlInfo',
'20:100/10 deadbeef', umsg.Attributes.Get('msgid'));
AssertEquals('seen-by from body',
'100/1 10', umsg.Attributes.Get('seen-by'));
AssertEquals('path from body',
'100/10', umsg.Attributes.Get('path'));
AssertTrue('body is plain text (Origin line survives)',
Pos('Origin: Node', umsg.Body) > 0);
AssertFalse('body no longer carries SEEN-BY:',
Pos('SEEN-BY:', umsg.Body) > 0);
AssertFalse('body no longer carries ^APATH:',
Pos(#1 + 'PATH:', umsg.Body) > 0);
finally
base.Free;
end;
TestOK;
end;
{ --- UpdateMessage tests (NR Message 17 / v0.6.0) ------------------
The linker use-case: after WriteMessage lands a message at some
Index, we fetch it back, mutate a reply-chain header field, call
UpdateMessage, close / reopen, read again, and assert the mutation
persisted. Body + other content must stay untouched. }
procedure TestUpdateMessageJam;
var
base: TMessageBase;
msg: TUniMessage;
begin
TestBegin('UpdateMessage: JAM header-only rewrite persists');
CleanDir(SCRATCH + '/upd_jam');
ForceDirectories(SCRATCH + '/upd_jam');
base := MessageBaseOpen(mbfJam, SCRATCH + '/upd_jam/echo', momCreate);
try
AssertTrue('open', base.Open);
msg.Attributes.Clear;
msg.Attributes.SetValue('from', 'Alice');
msg.Attributes.SetValue('to', 'Bob');
msg.Attributes.SetValue('subject', 'reply-chain update');
msg.Attributes.SetDate('date.written', Now);
msg.Attributes.SetValue('msgid', '1:1/1 abcd1234');
msg.Body := 'hello world';
AssertTrue('write', base.WriteMessage(msg));
{ Now mutate Reply1st / ReplyNext on the written message. }
AssertTrue('read back', base.ReadMessage(0, msg));
msg.Attributes.SetInt('jam.reply1st', 42);
msg.Attributes.SetInt('jam.replynext', 99);
AssertTrue('update', base.UpdateMessage(0, msg));
base.Close;
AssertTrue('reopen', base.Open);
AssertTrue('read after update', base.ReadMessage(0, msg));
AssertEquals('reply1st persisted', 42, msg.Attributes.GetInt('jam.reply1st', -1));
AssertEquals('replynext persisted', 99, msg.Attributes.GetInt('jam.replynext', -1));
{ JAM writer now appends a trailing CR per FTS-0001 (fix for
NR Bug #1); the body reads back with the CR still attached. }
AssertEquals('body untouched', 'hello world'#13, msg.Body);
AssertEquals('msgid untouched', '1:1/1 abcd1234', msg.Attributes.Get('msgid'));
AssertEquals('from untouched', 'Alice', msg.Attributes.Get('from'));
finally
base.Free;
end;
TestOK;
end;
procedure TestUpdateMessageSquish;
var
base: TMessageBase;
msg: TUniMessage;
rest: TStringDynArray;
begin
TestBegin('UpdateMessage: Squish header-only rewrite persists');
CleanDir(SCRATCH + '/upd_sq');
ForceDirectories(SCRATCH + '/upd_sq');
base := MessageBaseOpen(mbfSquish, SCRATCH + '/upd_sq/rc', momCreate);
try
AssertTrue('open', base.Open);
msg.Attributes.Clear;
msg.Attributes.SetValue('from', 'Alice');
msg.Attributes.SetValue('to', 'Bob');
msg.Attributes.SetValue('subject', 'reply-chain update');
msg.Attributes.SetDate('date.written', Now);
msg.Attributes.SetValue('msgid', '1:1/1 deadbeef');
msg.Body := 'hello squish';
AssertTrue('write', base.WriteMessage(msg));
AssertTrue('read back', base.ReadMessage(0, msg));
msg.Attributes.SetInt('squish.reply1st', 7);
msg.Attributes.SetList('squish.replynext',
TStringDynArray.Create('8', '9'));
AssertTrue('update', base.UpdateMessage(0, msg));
base.Close;
AssertTrue('reopen', base.Open);
AssertTrue('read after update', base.ReadMessage(0, msg));
AssertEquals('reply1st persisted', 7, msg.Attributes.GetInt('squish.reply1st', -1));
rest := msg.Attributes.GetList('squish.replynext');
AssertEquals('replynext count', 2, Length(rest));
AssertEquals('replynext[0]', '8', rest[0]);
AssertEquals('replynext[1]', '9', rest[1]);
AssertEquals('body untouched', 'hello squish', msg.Body);
AssertEquals('msgid untouched', '1:1/1 deadbeef', msg.Attributes.Get('msgid'));
AssertEquals('from untouched', 'Alice', msg.Attributes.Get('from'));
finally
base.Free;
end;
TestOK;
end;
procedure TestUpdateMessageMsg;
var
base: TMessageBase;
msg: TUniMessage;
begin
TestBegin('UpdateMessage: *.MSG header-only rewrite persists');
CleanDir(SCRATCH + '/upd_msg');
ForceDirectories(SCRATCH + '/upd_msg/');
base := MessageBaseOpen(mbfMsg, SCRATCH + '/upd_msg/', momCreate);
try
AssertTrue('open', base.Open);
msg.Attributes.Clear;
msg.Attributes.SetValue('from', 'Alice');
msg.Attributes.SetValue('to', 'Bob');
msg.Attributes.SetValue('subject', 'header update');
msg.Attributes.SetDate('date.written', Now);
msg.Body := 'hello netmail';
AssertTrue('write', base.WriteMessage(msg));
AssertTrue('read back', base.ReadMessage(0, msg));
msg.Attributes.SetInt('msg.replyto', 11);
msg.Attributes.SetInt('msg.nextreply', 22);
AssertTrue('update', base.UpdateMessage(0, msg));
base.Close;
AssertTrue('reopen', base.Open);
AssertTrue('read after update', base.ReadMessage(0, msg));
AssertEquals('replyto persisted', 11, msg.Attributes.GetInt('msg.replyto', -1));
AssertEquals('nextreply persisted', 22, msg.Attributes.GetInt('msg.nextreply', -1));
AssertEquals('body untouched', 'hello netmail', msg.Body);
AssertEquals('subject untouched', 'header update', msg.Attributes.Get('subject'));
AssertEquals('from untouched', 'Alice', msg.Attributes.Get('from'));
finally
base.Free;
end;
TestOK;
end;
procedure TestUpdateMessageHudson;
var
base: TMessageBase;
msg: TUniMessage;
begin
TestBegin('UpdateMessage: Hudson header-only rewrite persists');
CleanDir(SCRATCH + '/upd_hu');
ForceDirectories(SCRATCH + '/upd_hu');
base := MessageBaseOpen(mbfHudson, SCRATCH + '/upd_hu', momCreate);
try
AssertTrue('open', base.Open);
msg.Attributes.Clear;
msg.Attributes.SetInt('board', 1);
msg.Attributes.SetValue('from', 'Alice');
msg.Attributes.SetValue('to', 'Bob');
msg.Attributes.SetValue('subject', 'hudson update');
msg.Attributes.SetDate('date.written', Now);
msg.Body := 'hello hudson';
AssertTrue('write', base.WriteMessage(msg));
AssertTrue('read back', base.ReadMessage(0, msg));
msg.Attributes.SetInt('hudson.prevreply', 33);
msg.Attributes.SetInt('hudson.nextreply', 44);
AssertTrue('update', base.UpdateMessage(0, msg));
base.Close;
AssertTrue('reopen', base.Open);
AssertTrue('read after update', base.ReadMessage(0, msg));
AssertEquals('prevreply persisted', 33, msg.Attributes.GetInt('hudson.prevreply', -1));
AssertEquals('nextreply persisted', 44, msg.Attributes.GetInt('hudson.nextreply', -1));
AssertEquals('body untouched', 'hello hudson', msg.Body);
AssertEquals('subject untouched','hudson update', msg.Attributes.Get('subject'));
AssertEquals('from untouched', 'Alice', msg.Attributes.Get('from'));
finally
base.Free;
end;
TestOK;
end;
procedure TestUpdateMessageGoldBase;
var
base: TMessageBase;
msg: TUniMessage;
begin
TestBegin('UpdateMessage: GoldBase header-only rewrite persists');
CleanDir(SCRATCH + '/upd_gb');
ForceDirectories(SCRATCH + '/upd_gb');
base := MessageBaseOpen(mbfGoldBase, SCRATCH + '/upd_gb', momCreate);
try
AssertTrue('open', base.Open);
msg.Attributes.Clear;
msg.Attributes.SetInt('board', 1);
msg.Attributes.SetValue('from', 'Alice');
msg.Attributes.SetValue('to', 'Bob');
msg.Attributes.SetValue('subject', 'gold update');
msg.Attributes.SetDate('date.written', Now);
msg.Body := 'hello goldbase';
AssertTrue('write', base.WriteMessage(msg));
AssertTrue('read back', base.ReadMessage(0, msg));
msg.Attributes.SetInt('goldbase.prevreply', 55);
msg.Attributes.SetInt('goldbase.nextreply', 66);
AssertTrue('update', base.UpdateMessage(0, msg));
base.Close;
AssertTrue('reopen', base.Open);
AssertTrue('read after update', base.ReadMessage(0, msg));
AssertEquals('prevreply persisted', 55, msg.Attributes.GetInt('goldbase.prevreply', -1));
AssertEquals('nextreply persisted', 66, msg.Attributes.GetInt('goldbase.nextreply', -1));
AssertEquals('body untouched', 'hello goldbase', msg.Body);
AssertEquals('subject untouched','gold update', msg.Attributes.Get('subject'));
AssertEquals('from untouched', 'Alice', msg.Attributes.Get('from'));
finally
base.Free;
end;
TestOK;
end;
procedure TestUpdateMessagePCBoard;
var
base: TMessageBase;
msg: TUniMessage;
begin
TestBegin('UpdateMessage: PCBoard header-only rewrite persists');
CleanDir(SCRATCH + '/upd_pc');
ForceDirectories(SCRATCH + '/upd_pc');
base := MessageBaseOpen(mbfPCBoard, SCRATCH + '/upd_pc/GEN.MSG', momCreate);
try
AssertTrue('open', base.Open);
msg.Attributes.Clear;
msg.Attributes.SetValue('from', 'Alice');
msg.Attributes.SetValue('to', 'Bob');
msg.Attributes.SetValue('subject', 'pcb update');
msg.Attributes.SetDate('date.written', Now);
msg.Body := 'hello pcboard';
AssertTrue('write', base.WriteMessage(msg));
AssertTrue('read back', base.ReadMessage(0, msg));
msg.Attributes.SetInt('pcb.refnum', 77);
msg.Attributes.SetInt('pcb.extendedstatus', 42);
AssertTrue('update', base.UpdateMessage(0, msg));
base.Close;
AssertTrue('reopen', base.Open);
AssertTrue('read after update', base.ReadMessage(0, msg));
AssertEquals('refnum persisted', 77, msg.Attributes.GetInt('pcb.refnum', -1));
AssertEquals('extendedstatus persisted', 42, msg.Attributes.GetInt('pcb.extendedstatus', -1));
{ PCBoard uppercases WhoFrom/WhoTo at write time; that's the
native on-disk convention. We only assert the update didn't
corrupt the existing on-disk value. }
AssertEquals('subject untouched', 'pcb update', msg.Attributes.Get('subject'));
AssertEquals('from untouched', 'ALICE', msg.Attributes.Get('from'));
finally
base.Free;
end;
TestOK;
end;
procedure TestUpdateMessageEzyCom;
var
base: TMessageBase;
msg: TUniMessage;
begin
TestBegin('UpdateMessage: EzyCom header-only rewrite persists');
CleanDir(SCRATCH + '/upd_ez');
ForceDirectories(SCRATCH + '/upd_ez');
base := MessageBaseOpen(mbfEzyCom, SCRATCH + '/upd_ez', momCreate);
TEzyComMessageBase(base).Board := 1;
try
AssertTrue('open', base.Open);
msg.Attributes.Clear;
msg.Attributes.SetInt('board', 1);
msg.Attributes.SetValue('from', 'Alice');
msg.Attributes.SetValue('to', 'Bob');
msg.Attributes.SetValue('subject', 'ezy update');
msg.Attributes.SetDate('date.written', Now);
msg.Body := 'hello ezy';
AssertTrue('write', base.WriteMessage(msg));
AssertTrue('read back', base.ReadMessage(0, msg));
msg.Attributes.SetInt('ezy.prevreply', 88);
msg.Attributes.SetInt('ezy.nextreply', 99);
AssertTrue('update', base.UpdateMessage(0, msg));
base.Close;
AssertTrue('reopen', base.Open);
AssertTrue('read after update', base.ReadMessage(0, msg));
AssertEquals('prevreply persisted', 88, msg.Attributes.GetInt('ezy.prevreply', -1));
AssertEquals('nextreply persisted', 99, msg.Attributes.GetInt('ezy.nextreply', -1));
AssertEquals('body untouched', 'hello ezy', msg.Body);
AssertEquals('subject untouched', 'ezy update', msg.Attributes.Get('subject'));
AssertEquals('from untouched', 'Alice', msg.Attributes.Get('from'));
finally
base.Free;
end;
TestOK;
end;
{ Wildcat can't be created from scratch (the WC SDK needs a full
WILDCAT.CFG / conference layout), so this test exercises the
update path on the vendored sample base at tests/data/wildcat/.
If the sample is missing -- fresh checkout, non-standard layout --
the test SKIPs rather than fails. }
procedure TestUpdateMessageWildcat;
const
SRC = 'tests/data/wildcat';
WC_SCRATCH = '/tmp/ma_wildcat_upd';
var
base: TMessageBase;
adapter: TWildcatMessageBase;
msg: TUniMessage;
origCost, newCost: longint;
begin
TestBegin('UpdateMessage: Wildcat header-only rewrite persists');
if not DirectoryExists(SRC) then begin
WriteLn('SKIP (sample at ', SRC, ' missing)');
exit;
end;
if DirectoryExists(WC_SCRATCH) then
ExecuteProcess('/bin/sh', ['-c', 'rm -rf "' + WC_SCRATCH + '"']);
ForceDirectories(WC_SCRATCH);
if ExecuteProcess('/bin/sh',
['-c', 'cp -r "' + SRC + '"/. "' + WC_SCRATCH + '"/']) <> 0 then begin
WriteLn('SKIP (cp -r failed)');
exit;
end;
base := MessageBaseOpen(mbfWildcat, WC_SCRATCH, momReadWrite);
adapter := base as TWildcatMessageBase;
adapter.Conference := 4; { sample has messages in confs 4, 5, 6 }
try
AssertTrue('open', base.Open);
if base.MessageCount = 0 then begin
WriteLn('SKIP (sample conference 4 has no messages)');
exit;
end;
AssertTrue('read', base.ReadMessage(0, msg));
origCost := msg.Attributes.GetInt('cost', 0);
newCost := origCost + 12345;
msg.Attributes.SetInt('cost', newCost);
AssertTrue('update', base.UpdateMessage(0, msg));
base.Close;
AssertTrue('reopen', base.Open);
AssertTrue('read after update', base.ReadMessage(0, msg));
AssertEquals('cost persisted', newCost, msg.Attributes.GetInt('cost', -1));
finally
base.Free;
end;
TestOK;
end;
{ ============================================================
NR v0.6.1 bug reports (joint inbox Message 19)
============================================================ }
{ NR Bug #3a: JAM MsgIdCRC must be computed (CRC32 of lowercased
msgid), not left as zero. External JAM readers (GoldED,
MsgEd, hptlink) walk the reply chain via MsgIdCRC matching;
zero breaks threading across every tool that isn't us. }
procedure TestJamCrcComputed;
const
MSGID_VAL = '1:123/456 abcdef01';
var
base: TMessageBase;
adapter: TJamMessageBase;
umsg: TUniMessage;
idx: JamIdxRec;
hdr: JamHdr;
expectedCRC: longint;
begin
TestBegin('JAM MsgIdCRC computed from msgid (NR Bug #3a)');
CleanDir(SCRATCH + '/jam_crc');
ForceDirectories(SCRATCH + '/jam_crc');
base := MessageBaseOpen(mbfJam, SCRATCH + '/jam_crc/echo', momCreate);
adapter := base as TJamMessageBase;
try
AssertTrue('open', base.Open);
umsg.Attributes.Clear;
umsg.Attributes.SetValue('from', 'Alice');
umsg.Attributes.SetValue('to', 'Bob');
umsg.Attributes.SetValue('subject', 'crc-check');
umsg.Attributes.SetDate('date.written', Now);
umsg.Attributes.SetValue('msgid', MSGID_VAL);
umsg.Body := 'hello';
AssertTrue('write', base.WriteMessage(umsg));
base.Close;
{ Verify on disk that MsgIdCRC is the expected CRC32 value
and PasswordCRC is the no-password sentinel. }
expectedCRC := TJamBase.CalcMsgIdCRC(MSGID_VAL);
AssertTrue('reopen', base.Open);
AssertTrue('read index', adapter.Native.ReadIndex(0, idx));
AssertTrue('read header', adapter.Native.ReadHeader(idx.HdrOffset, hdr));
AssertEquals('MsgIdCRC = CRC32(lowercase msgid)',
expectedCRC, hdr.MsgIdCRC);
AssertEquals('ReplyCRC = 0xFFFFFFFF (no REPLY kludge)',
longint($FFFFFFFF), hdr.ReplyCRC);
AssertEquals('PasswordCRC = 0xFFFFFFFF (no password)',
longint($FFFFFFFF), hdr.PasswordCRC);
finally
base.Free;
end;
TestOK;
end;
{ NR Bug #1: trailing CR on body must be preserved through
JAM write. HPT keeps it; the library was stripping it. }
procedure TestJamBodyTrailingCR;
var
base: TMessageBase;
adapter: TJamMessageBase;
umsg: TUniMessage;
idx: JamIdxRec;
hdr: JamHdr;
stored: AnsiString;
begin
TestBegin('JAM body preserves trailing CR on write (NR Bug #1)');
CleanDir(SCRATCH + '/jam_cr');
ForceDirectories(SCRATCH + '/jam_cr');
base := MessageBaseOpen(mbfJam, SCRATCH + '/jam_cr/echo', momCreate);
adapter := base as TJamMessageBase;
try
AssertTrue('open', base.Open);
umsg.Attributes.Clear;
umsg.Attributes.SetValue('from', 'A');
umsg.Attributes.SetValue('to', 'B');
umsg.Attributes.SetValue('subject', 'cr-check');
umsg.Attributes.SetDate('date.written', Now);
{ Body without trailing CR -- library should add one. }
umsg.Body := 'No CR here';
AssertTrue('write', base.WriteMessage(umsg));
base.Close;
AssertTrue('reopen', base.Open);
AssertTrue('read index', adapter.Native.ReadIndex(0, idx));
AssertTrue('read header', adapter.Native.ReadHeader(idx.HdrOffset, hdr));
stored := adapter.Native.ReadBody(hdr.TxtOffset, hdr.TxtLen);
AssertTrue('body ends with #13',
(Length(stored) > 0) and (stored[Length(stored)] = #13));
finally
base.Free;
end;
TestOK;
end;
{ NR Bug #4b: CHRS and TID attrs must survive JAM write as
JAM_FTSKLUDGE subfields. Losing CHRS corrupts non-ASCII
display. }
procedure TestJamChrsTidRoundTrip;
var
base: TMessageBase;
umsg: TUniMessage;
begin
TestBegin('JAM preserves CHRS + TID via FTSKLUDGE subfield (NR Bug #4b)');
CleanDir(SCRATCH + '/jam_chrs');
ForceDirectories(SCRATCH + '/jam_chrs');
base := MessageBaseOpen(mbfJam, SCRATCH + '/jam_chrs/echo', momCreate);
try
AssertTrue('open', base.Open);
umsg.Attributes.Clear;
umsg.Attributes.SetValue('from', 'A');
umsg.Attributes.SetValue('to', 'B');
umsg.Attributes.SetValue('subject', 'chrs-test');
umsg.Attributes.SetDate('date.written', Now);
umsg.Attributes.SetValue('chrs', 'LATIN-1 2');
umsg.Attributes.SetValue('tid', 'TestTosser 1.0');
umsg.Body := 'body';
AssertTrue('write', base.WriteMessage(umsg));
base.Close;
AssertTrue('reopen', base.Open);
AssertTrue('read', base.ReadMessage(0, umsg));
AssertEquals('chrs round-trips',
'LATIN-1 2', umsg.Attributes.Get('chrs'));
AssertEquals('tid round-trips',
'TestTosser 1.0', umsg.Attributes.Get('tid'));
finally
base.Free;
end;
TestOK;
end;
{ Hudson (and by extension GoldBase/PCBoard/EzyCom/Wildcat):
MSGID / PID / SEEN-BY / PATH / etc. now round-trip through
SplitKludgeBlob on Read + BuildKludgePrefix on Write, so a
consumer can read the attr bag and get real values. }
procedure TestHudsonKludgeRoundTrip;
var
base: TMessageBase;
umsg: TUniMessage;
begin
TestBegin('Hudson kludge round-trip (MSGID/PID/SEEN-BY/CHRS)');
CleanDir(SCRATCH + '/hu_kludge');
ForceDirectories(SCRATCH + '/hu_kludge');
base := MessageBaseOpen(mbfHudson, SCRATCH + '/hu_kludge', momCreate);
try
AssertTrue('open', base.Open);
umsg.Attributes.Clear;
umsg.Attributes.SetInt('board', 1);
umsg.Attributes.SetValue('from', 'Alice');
umsg.Attributes.SetValue('to', 'Bob');
umsg.Attributes.SetValue('subject', 'kludge test');
umsg.Attributes.SetDate('date.written', Now);
umsg.Attributes.SetValue('msgid', '1:1/1 deadbeef');
umsg.Attributes.SetValue('pid', 'TestEditor 1.0');
umsg.Attributes.SetValue('chrs', 'UTF-8 4');
umsg.Attributes.SetValue('tzutc', '0100');
umsg.Attributes.SetValue('seen-by', '1/1 2');
umsg.Attributes.SetValue('path', '1/1');
umsg.Body := 'user text';
AssertTrue('write', base.WriteMessage(umsg));
base.Close;
AssertTrue('reopen', base.Open);
AssertTrue('read', base.ReadMessage(0, umsg));
AssertEquals('msgid', '1:1/1 deadbeef', umsg.Attributes.Get('msgid'));
AssertEquals('pid', 'TestEditor 1.0', umsg.Attributes.Get('pid'));
AssertEquals('chrs', 'UTF-8 4', umsg.Attributes.Get('chrs'));
AssertEquals('tzutc', '0100', umsg.Attributes.Get('tzutc'));
AssertEquals('seen-by', '1/1 2', umsg.Attributes.Get('seen-by'));
AssertEquals('path', '1/1', umsg.Attributes.Get('path'));
AssertEquals('body is user text only',
'user text', umsg.Body);
finally
base.Free;
end;
TestOK;
end;
{ GoldBase shares the pattern -- spot-check msgid + seen-by. }
procedure TestGoldBaseKludgeRoundTrip;
var
base: TMessageBase;
umsg: TUniMessage;
begin
TestBegin('GoldBase kludge round-trip (msgid + seen-by)');
CleanDir(SCRATCH + '/gb_kludge');
ForceDirectories(SCRATCH + '/gb_kludge');
base := MessageBaseOpen(mbfGoldBase, SCRATCH + '/gb_kludge', momCreate);
try
AssertTrue('open', base.Open);
umsg.Attributes.Clear;
umsg.Attributes.SetInt('board', 1);
umsg.Attributes.SetValue('from', 'Alice');
umsg.Attributes.SetValue('to', 'Bob');
umsg.Attributes.SetValue('subject', 'gb kludge test');
umsg.Attributes.SetDate('date.written', Now);
umsg.Attributes.SetValue('msgid', '2:2/2 beefcafe');
umsg.Attributes.SetValue('seen-by', '2/2 5');
umsg.Body := 'gb body';
AssertTrue('write', base.WriteMessage(umsg));
base.Close;
AssertTrue('reopen', base.Open);
AssertTrue('read', base.ReadMessage(0, umsg));
AssertEquals('msgid', '2:2/2 beefcafe', umsg.Attributes.Get('msgid'));
AssertEquals('seen-by', '2/2 5', umsg.Attributes.Get('seen-by'));
AssertEquals('body is user text only', 'gb body', umsg.Body);
finally
base.Free;
end;
TestOK;
end;
begin
WriteLn('fpc-msgbase: 0.3.5 consumer-feedback regression tests');
WriteLn;
ForceDirectories(SCRATCH);
TestIntlRoundTrip(mbfJam, SCRATCH + '/intl/jam/echo',
'INTL/FMPT/TOPT round-trip: JAM');
TestIntlRoundTrip(mbfSquish, SCRATCH + '/intl/squish/sq',
'INTL/FMPT/TOPT round-trip: Squish');
TestIntlRoundTrip(mbfMsg, SCRATCH + '/intl/msg/',
'INTL/FMPT/TOPT round-trip: MSG');
TestAreaAutoPop;
TestGetSetListAccessors;
TestSyncWriteable;
TestSquishReplyChain;
TestSquishHptBodyEpilogue;
TestUpdateMessageJam;
TestUpdateMessageSquish;
TestUpdateMessageMsg;
TestUpdateMessageHudson;
TestUpdateMessageGoldBase;
TestUpdateMessagePCBoard;
TestUpdateMessageEzyCom;
TestUpdateMessageWildcat;
TestJamCrcComputed;
TestJamBodyTrailingCR;
TestJamChrsTidRoundTrip;
TestHudsonKludgeRoundTrip;
TestGoldBaseKludgeRoundTrip;
Halt(TestsSummary);
end.