Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ call it after a reply to catch a stray byte or a second status line (e.g. a NUL
| `(*Server) TLSHandshakes()`, `TLSFailed()`, `TLSServerNames()` | completed and failed handshakes (implicit TLS and STARTTLS), SNI of every ClientHello |
| `LineHandler(f func(c *Conn, line string) (reply []string, closeConn bool)) func(*Conn)` | answer each client line with what `f` returns, in any order (what `Script` cannot); `f` may `c.ReadDotBlock()` for an article; `Faults` apply |
| `PeerHandler(*PeerOptions) func(*Conn)`, `type PeerOptions` | transit peer: MODE STREAM, CHECK, TAKETHIS (RFC 4644), IHAVE (RFC 3977 6.3.2), AUTHINFO; options below |
| `ReplyCorpus() []ReplyCase`, `ReplyCaseByName(name) (ReplyCase, bool)` | 112 named wrong and malformed server replies for the test suite of a client, see below |
| `ReplyCorpus() []ReplyCase`, `ReplyCaseByName(name) (ReplyCase, bool)` | 125 named wrong and malformed server replies for the test suite of a client, see below |
| `(*Conn) Wait(ch <-chan struct{}) bool`, `Sleep(d) bool` | for a Handler or `PeerOptions.BeforeReply`: block until `ch` fires (true) or the Deadline passes / the server closes (false); `Sleep` is bounded the same way |
| `NewStore() *Store` (the zero value works too) | stateful store; safe for concurrent use; returns copies |
| `(*Store) AddGroup`, `AddGroupAt`, `SetPosting`, `AddArticle(group, Article) int64` | setup |
Expand Down Expand Up @@ -396,27 +396,28 @@ Faults on the COMPRESS reply (`DropAfterBytes`, `Split` offsets) count the `Empt
`With206` bytes too. `compress/flate` cannot flush partially and its reader holds `Z_PARTIAL_FLUSH`
data back until more arrives; the kit writes the partial flush itself, and its client inflater reads it.

**Reply corpus.** `fakeserver.ReplyCorpus()` is 112 named wrong and malformed server replies (GROUP
**Reply corpus.** `fakeserver.ReplyCorpus()` is 125 named wrong and malformed server replies (GROUP
answered 215, a 211 without numbers, a greeting of `hello there`, an OVER row with a letter for the
article number) for the test suite of an NNTP client, so each client project does not write the
table again; `ReplyCaseByName(name)` finds one. A `ReplyCase` is `{Name; Family; Cmd, Line string;
Lines []string; Why string; Want Outcome; NeedsTimeout bool}`:
Lines []string; Why string; Want Outcome; NeedsTimeout, Final bool}`:

| field / method | what |
|---|---|
| `Name`, `Why` | a unique short identifier fit for `t.Run` (`group-211-nofields`); what is wrong, with the RFC where there is one |
| `Family` | `FamilyCode` (well-formed reply, wrong code or shape for the command: 37 cases), `FamilyStatus` (malformed status line: 37), `FamilyGreeting` (bad greeting: 10), `FamilyBlock` (bad rows or terminator of a multi-line reply: 28) |
| `Family` | `FamilyCode` (well-formed reply, wrong code or shape for the command: 39 cases), `FamilyStatus` (malformed status line: 37), `FamilyGreeting` (bad greeting: 10), `FamilyBlock` (bad rows or terminator of a multi-line reply: 39) |
| `Cmd`, `Line` | the `Faults` key (upper-cased command) whose reply is replaced, and a sample client line (`GROUP g`); both empty for a greeting case |
| `Lines` | written instead of the normal reply, each plus CRLF (a block carries its own `.` line); may hold bytes that are no valid NNTP (NUL, 8-bit, bare LF) |
| `Want` | the generic expected client outcome: `WantError` (the call fails cleanly: no panic, no hang, no data handed back as good; then the connection is in sync or closed; 74 cases), `WantEither` (a strict client fails, a lenient one may accept; neither may panic, hang or desynchronise; 34), `WantAccept` (odd but legal, e.g. `high` below `low` for an empty group, 281 to AUTHINFO USER; the client must accept it; 4). Error types and reuse of the connection afterwards are for each client suite to agree on |
| `Want` | the generic expected client outcome: `WantError` (the call fails cleanly: no panic, no hang, no data handed back as good; then the connection is in sync or closed; 81 cases), `WantEither` (a strict client fails, a lenient one may accept; neither may panic, hang or desynchronise; 38), `WantAccept` (odd but legal, e.g. `high` below `low` for an empty group, 281 to AUTHINFO USER, an address in the 240 text of POST; the client must accept it; 6). Error types and reuse of the connection afterwards are for each client suite to agree on |
| `NeedsTimeout` | the client waits for more data than the reply holds (a terminator that never comes): the test needs a read timeout and the call ends with one |
| `Apply(*Options)`, `ApplyLine(*Options)` | `Faults.Override[Cmd]` (every line of that command) or `Faults.OverrideLine[Line]` (that line only); `Options.Greeting` for a greeting case; they add to the maps already in `o`. A command case needs a mode that applies `Faults`, e.g. `Options.Store` |
| `Final` | `Lines` replace the reply after the article of POST or IHAVE (`Faults.OverrideFinal`): the client gets its normal 340 or 335, sends an article, then reads the case. The corpus's Final cases are POST and need a Store that allows posting |
| `Apply(*Options)`, `ApplyLine(*Options)` | `Faults.Override[Cmd]` (every line of that command) or `Faults.OverrideLine[Line]` (that line only); `Faults.OverrideFinal[Cmd]` for a `Final` case (both); `Options.Greeting` for a greeting case; they add to the maps already in `o`. A command case needs a mode that applies `Faults`, e.g. `Options.Store` |
| `Bytes() []byte`, `IsGreeting() bool` | exactly what the server writes for the case; whether it is a greeting case (the reply to connecting) |

A missing `.` terminator is `Faults.Endless`, an overlong block line `Faults.LongLine`, a reply cut
short `Faults.DropInMultiline`: not repeated in the corpus. A reply that depends on an earlier answer of the
same exchange (POST answered 340 and then 340 again after the article) needs a Script or a Handler and is not
in it.
in it; a fixed final reply after the article is a `Final` case.

```go
for _, rc := range fakeserver.ReplyCorpus() {
Expand Down Expand Up @@ -658,6 +659,7 @@ into a `fakeserver.Article` body like any other: no CRLF and no dot-stuffing (th
| `EncodeParts(name, data, partSize, Options) [][]string` | one post per part: `=ybegin part= total=`, `=ypart begin= end=`, the data, `=yend size= part= pcrc32=` (and `crc32=` of the whole data on the last part) |
| `Data(Profile, size, seed) []byte`; `Random`, `Plain`, `Escapes`, `Edges` | reproducible data: random bytes (most of Usenet), no escapes, an escape in every second byte, a third of the bytes at `.`, tab and space |
| `Generator(Profile, low, high, size func(n int64) int, Options) fakeserver.Generator` | a generated group: article n is `Data(p, size(n), n)` named `<p>-<n>.bin`, with `GeneratedArticle`'s headers and a yEnc Subject |
| `Corpus() []Case`, `CaseByName(name) (Case, bool)`; `type Case struct { Name string; Lines []string; Data []byte; Why string; Want fakeserver.Outcome }` | broken and odd posts for the test suite of a decoder, with the reply corpus's outcome classes: a stray `=yend` after a complete block and text after `=yend` (accept, decode to `Data`), a bad CRC or size (error), no `=yend` (either) |

`RawDots` and `RawSpaces` model posting tools that leave those characters to the server's
dot-stuffing and to the reader. The broken-post options model corrupt uploads; `Faults.DropInMultiline`
Expand Down
7 changes: 4 additions & 3 deletions fakeserver/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@
// wrong; Apply puts it into the Options of a fake server (a command case needs a mode that
// applies Faults, e.g. a Store):
//
// func ReplyCorpus() []ReplyCase // 112 cases
// func ReplyCorpus() []ReplyCase // 125 cases
// func ReplyCaseByName(name string) (ReplyCase, bool)
// type ReplyCase struct {
// Name string // unique, fit for t.Run, e.g. "group-211-nofields"
Expand All @@ -265,9 +265,10 @@
// Why string // what is wrong, with the RFC where there is one
// Want Outcome // WantError (fail cleanly), WantEither (strict fails, lenient may accept; no panic, hang or desync), WantAccept (odd but legal: must be accepted)
// NeedsTimeout bool // the client waits for more data than the reply holds: the test needs a read timeout
// Final bool // Lines replace the reply after the article of POST/IHAVE (Faults.OverrideFinal): the client gets 340/335, sends an article, then reads the case
// }
// func (rc ReplyCase) Apply(o *Options) // Faults.Override[Cmd]; Options.Greeting for a greeting case; adds to the maps already in o
// func (rc ReplyCase) ApplyLine(o *Options) // Apply for Line only (Faults.OverrideLine[Line])
// func (rc ReplyCase) Apply(o *Options) // Faults.Override[Cmd]; OverrideFinal[Cmd] for a Final case; Options.Greeting for a greeting case; adds to the maps already in o
// func (rc ReplyCase) ApplyLine(o *Options) // Apply for Line only (Faults.OverrideLine[Line]); a Final case as Apply
// func (rc ReplyCase) Bytes() []byte // exactly what the server writes: every line + CRLF
// func (rc ReplyCase) IsGreeting() bool
//
Expand Down
42 changes: 39 additions & 3 deletions fakeserver/replycorpus.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ import "strings"
// A missing "." terminator is Faults.Endless, an overlong block line Faults.LongLine, a reply
// cut short Faults.DropInMultiline: not repeated here. A reply that depends on an earlier
// answer of the same exchange (POST answered 340 and then, after the article, 340 again) needs
// a Script or a Handler, not an Override, and is not in the corpus.
// a Script or a Handler, not an Override, and is not in the corpus. A case with Final set
// replaces the reply that follows the article of POST or IHAVE (240/441, 235/437) instead: the
// first reply is the normal 340 or 335, and the test sends an article before it looks.

// Family groups the cases of the reply corpus.
type Family string
Expand Down Expand Up @@ -97,6 +99,13 @@ type ReplyCase struct {
// terminator that never comes): the test needs a read timeout in the client, and the
// call ends with a timeout error.
NeedsTimeout bool
// Final is set when Lines replace the reply that follows the article of POST or IHAVE
// (240/441, 235/437; Faults.OverrideFinal), not the first reply: the client gets its
// normal 340 or 335, sends an article and then reads the case. ApplyLine treats it like
// Apply, since OverrideFinal is keyed by the command only. The corpus's Final cases are
// POST: they need a Store that allows posting (not ReaderOnly); Script, LineHandler and
// Handler modes do not apply OverrideFinal.
Final bool
}

// IsGreeting reports whether the case is a bad greeting (the reply to connecting, not to a
Expand All @@ -107,7 +116,8 @@ func (rc ReplyCase) IsGreeting() bool { return rc.Family == FamilyGreeting }
func (rc ReplyCase) Bytes() []byte { return joinCRLF(rc.Lines) }

// Apply makes the server of o write the case: Faults.Override[Cmd] for a command case (every
// line of that command gets the reply), Options.Greeting for a greeting case. It adds to the
// line of that command gets the reply), Faults.OverrideFinal[Cmd] for a Final case,
// Options.Greeting for a greeting case. It adds to the
// maps already in o and keeps the other faults. The server still needs a mode that applies
// Faults: a Store (Options.Store) for a command case; a greeting case works in any mode that
// writes Options.Greeting.
Expand All @@ -116,6 +126,13 @@ func (rc ReplyCase) Apply(o *Options) {
o.Greeting = strings.Join(rc.Lines, "\r\n")
return
}
if rc.Final {
if o.Faults.OverrideFinal == nil {
o.Faults.OverrideFinal = make(map[string][]string)
}
o.Faults.OverrideFinal[rc.Cmd] = append([]string(nil), rc.Lines...)
return
}
if o.Faults.Override == nil {
o.Faults.Override = make(map[string][]string)
}
Expand All @@ -125,7 +142,7 @@ func (rc ReplyCase) Apply(o *Options) {
// ApplyLine is Apply for the sample client line only (Faults.OverrideLine[Line]): other lines
// of the same command get their normal reply.
func (rc ReplyCase) ApplyLine(o *Options) {
if rc.IsGreeting() {
if rc.IsGreeting() || rc.Final {
rc.Apply(o)
return
}
Expand Down Expand Up @@ -166,6 +183,11 @@ func rcGreeting(name string, want Outcome, why string, lines ...string) ReplyCas
return ReplyCase{Name: name, Family: FamilyGreeting, Lines: lines, Why: why, Want: want}
}

func rcFinal(rc ReplyCase) ReplyCase {
rc.Final = true
return rc
}

func rcTimeout(rc ReplyCase) ReplyCase {
rc.NeedsTimeout = true
return rc
Expand Down Expand Up @@ -219,6 +241,9 @@ func ReplyCorpus() []ReplyCase {
rcCase(c, "ihave-340-first", "IHAVE <i@x>", WantError, "340 is the POST send code; IHAVE answers 335 (6.3.2)", "340 ok"),
rcCase(c, "quit-200", "QUIT", WantError, "QUIT answers 205 (5.4)", "200 hm"),
rcCase(c, "authinfo-user-250", "AUTHINFO USER u", WantError, "AUTHINFO USER answers 381, 281 or 481 (RFC 4643 2.3)", "250 hm"),
// code, Final: the reply after the article of POST, a well-formed 240 with odd text
rcFinal(rcCase(c, "post-240-address-in-text", "POST", WantAccept, "the 240 text is free and not defined to carry a message-id (6.3.1); an address in angle brackets there is not the Message-ID of the article", "240 posted ok; report abuse to <abuse@example.net>")),
rcFinal(rcCase(c, "post-240-empty-brackets", "POST", WantAccept, "the 240 text is free (6.3.1); '<>' in it is no message-id (RFC 5536 3.1.3 needs content between the brackets)", "240 <> posted")),
rcCase(c, "authinfo-user-215-block", "AUTHINFO USER u", WantError, "a 215 block where AUTHINFO USER answers one line (RFC 4643 2.3)", rcBlock("215 what", "111 19990101000000")...),
rcCase(c, "authinfo-user-281", "AUTHINFO USER u", WantAccept, "281 to USER with no PASS asked: legal when the server needs no password (RFC 4643 2.3.1); a client must not send PASS", "281 ok"),

Expand Down Expand Up @@ -285,6 +310,17 @@ func ReplyCorpus() []ReplyCase {
rcCase(b, "over-non-utf8-subject", "OVER 1-2", WantEither, "bytes that are not UTF-8 in the Subject field; a client keeps them or reports an error", rcBlock("224 overview follows", "1\tsubj \xff\xfe\ta@b\td\t<1@x>\t\t1\t1")...),
rcCase(b, "hdr-nonnumeric-row", "HDR Subject 1", WantError, "a word for the article number of an HDR row (8.5)", rcBlock("225 hdr", "abc x")...),
rcCase(b, "hdr-row-no-space", "HDR Subject 1", WantEither, "an HDR row of the number only, no value and no space (8.5)", rcBlock("225 hdr", "1")...),
rcCase(b, "hdr-msgid-row-for-range", "HDR Subject 1-2", WantEither, "a message-id where the article number belongs: rows of a range request carry numbers, the 0 form is for a message-id argument only (8.5.2); a client that checks the row syntax but not which request it answers may accept it", rcBlock("225 hdr", "<a@b> x")...),
rcCase(b, "hdr-msgid-unclosed-row", "HDR Subject <a@b>", WantError, "a row that starts with '<' but has no closing '>'; the row for a message-id argument starts with 0 (8.5.2)", rcBlock("225 hdr", "<broken value")...),
rcCase(b, "hdr-msgid-empty-row", "HDR Subject <a@b>", WantError, "'<>' where the 0 of a message-id argument belongs (8.5.2)", rcBlock("225 hdr", "<> value")...),
rcCase(b, "hdr-negative-row", "HDR Subject 1-2", WantError, "a negative article number in an HDR row (8.5.2: numbers are digits only)", rcBlock("225 hdr", "-1 x")...),
rcCase(b, "hdr-plus-sign-row", "HDR Subject 1-2", WantEither, "a '+' before the article number (8.5.2: digits only); common number parsers (strconv.Atoi, strtol, int()) accept it and get the right number", rcBlock("225 hdr", "+1 x")...),
rcCase(b, "hdr-underscore-row", "HDR Subject 1-2", WantError, "'1_0' for the article number: some number parsers (Python int(), Go ParseInt with base 0) accept the underscore and read 10, a wrong number handed back as good (8.5.2: digits only)", rcBlock("225 hdr", "1_0 x")...),
rcCase(b, "hdr-non-ascii-digit-row", "HDR Subject 1-2", WantError, "U+0661 (Arabic-Indic digit one) for the article number: Unicode-aware parsers read it as 1 (8.5.2: ASCII digits only)", rcBlock("225 hdr", "\u0661 x")...),
rcCase(b, "list-overview-fmt-empty-name", "LIST OVERVIEW.FMT", WantError, "a field of ':' alone, with no header name (8.4.2)", rcBlock("215 fmt", "Subject:", "From:", "Date:", "Message-ID:", "References:", ":bytes", ":lines", ":")...),
rcCase(b, "list-overview-fmt-bare-full", "LIST OVERVIEW.FMT", WantError, "' :full' with no header name before the suffix, also after trimming the space (8.4.2: ':full' follows a header name)", rcBlock("215 fmt", "Subject:", "From:", "Date:", "Message-ID:", "References:", ":bytes", ":lines", " :full")...),
rcCase(b, "list-overview-fmt-space-before-colon", "LIST OVERVIEW.FMT", WantEither, "'Xref :full', a space between the header name and the colon (8.4.2); a lenient client trims it to Xref:full, none may key the field 'Xref '", rcBlock("215 fmt", "Subject:", "From:", "Date:", "Message-ID:", "References:", ":bytes", ":lines", "Xref :full")...),
rcCase(b, "list-overview-fmt-leading-space", "LIST OVERVIEW.FMT", WantEither, "' Subject:', a space before the header name (8.4.2); a lenient client trims it, none may key the field ' Subject'", rcBlock("215 fmt", " Subject:", "From:", "Date:", "Message-ID:", "References:", ":bytes", ":lines")...),
rcCase(b, "list-active-too-few-fields", "LIST ACTIVE", WantError, "a LIST ACTIVE row without the status field (7.6.3)", rcBlock("215 list", "alt.test 5 1")...),
rcCase(b, "list-active-nonnumeric", "LIST ACTIVE", WantError, "letters for high and low (7.6.3)", rcBlock("215 list", "alt.test x y y")...),
rcCase(b, "list-active-empty-group", "LIST ACTIVE", WantAccept, "high below low is how an empty group reads (7.6.3): not an error", rcBlock("215 list", "alt.test 0 1 y")...),
Expand Down
Loading
Loading