Artifact 0925072c823ed1bc60cda17aff82c7dafbd1bebf76fe3d8210899762c11e7201:
- File
internal/xfer/message_contenttype_test.go
— part of check-in
[47dc6363c0]
at
2026-10-12 19:01:46
on branch main
— fix(sync): frame HTTP requests and replies as fossil's http.c does (#310) (#342)
The server refused application/x-fossil-debug with HTTP 415, where fossil dispatches it to sync like the other x-fossil types. The client read any 2xx as a sync reply and decoded a reply as compressed only when typed exactly application/x-fossil, so a reply with no Content-Type was read as plain cards and a proxy's HTML page was parsed as cards.
Replies now go through xfer.DecodeReply, which applies section 4's reply rule by case-insensitive prefix: no type is the compressed container, an x-fossil-debug or x-fossil-uncompressed prefix is card text, another x-fossil prefix is compressed, and anything else is refused. Only status 200 is a reply. (user: [email protected], size: 5760) [annotate] [blame] [check-ins using]
package xfer import ( "strings" "testing" ) // TestContentTypeIsCompressed pins the §4 media-type-to-framing rule: only // application/x-fossil selects the compressed container; every other type, // including application/x-fossil-uncompressed and an absent header, selects // plain card text. Method: map each media type and assert the boolean, which // is the whole of the dispatch decision. Case and trailing parameters are // ignored because §4 signals framing by the bare media type. func TestContentTypeIsCompressed(t *testing.T) { cases := []struct { contentType string compressed bool }{ {ContentTypeCompressed, true}, {"application/x-fossil", true}, {"APPLICATION/X-FOSSIL", true}, {"application/x-fossil; charset=utf-8", true}, {" application/x-fossil ", true}, {ContentTypeUncompressed, false}, {"application/x-fossil-uncompressed", false}, {"application/x-fossil-uncompressed; charset=utf-8", false}, {"text/plain", false}, {"", false}, } for _, c := range cases { if got := contentTypeIsCompressed(c.contentType); got != c.compressed { t.Errorf("contentTypeIsCompressed(%q) = %v, want %v", c.contentType, got, c.compressed) } } } // TestDecodeDispatchesOnContentType pins that framing follows the Content-Type, // not the bytes: a compressed container decodes to its real cards when labelled // compressed, and plain card text decodes when labelled uncompressed but is // refused when labelled compressed because it is not a zlib stream. Method: // build a container of known card count and plain card text, decode each under // its own label, and confirm the plain text is refused under the compressed // label rather than mis-decompressed. // // The reverse pairing -- a compressed body under the uncompressed label -- is // deliberately not asserted: DecodeUncompressed reads whatever lines it can and // does not reliably fault on binary input, so its output is unspecified. What // matters for §4 is that the label chooses the path, which the compressed-label // cases establish. func TestDecodeDispatchesOnContentType(t *testing.T) { compressed := compressedContainer(t, 4096) plaintext := []byte("igot " + strings.Repeat("a", 40) + "\nclone_seqno 3\n") if _, err := Decode(compressed, ContentTypeCompressed); err != nil { t.Errorf("compressed body under compressed type: %v", err) } msg, err := Decode(plaintext, ContentTypeUncompressed) if err != nil { t.Errorf("plain cards under uncompressed type: %v", err) } else if len(msg.Cards) != 2 { t.Errorf("plain cards under uncompressed type: %d cards, want 2", len(msg.Cards)) } if _, err := Decode(plaintext, ContentTypeCompressed); err == nil { t.Error("plain cards under compressed type decoded; they are not a zlib stream") } } // TestDecodeCompressedFailureNamesDecompression pins acceptance criterion 2 of // issue #106: a body whose Content-Type says compressed and which fails to // decompress reports the decompression fault, and is never retried as card // text. Method: build a body with a small in-bound declared length followed by // bytes that are not a zlib stream, so the failure occurs at inflation rather // than at the length check, and assert the error names the zlib failure and not // a card-parse failure. func TestDecodeCompressedFailureNamesDecompression(t *testing.T) { // Declared length 100 (well under the bound) so the length guard passes and // control reaches inflation; the payload after the prefix is not zlib. body := append([]byte{0x00, 0x00, 0x00, 0x64}, []byte("this is not a zlib stream at all")...) _, err := Decode(body, ContentTypeCompressed) if err == nil { t.Fatal("Decode accepted a non-zlib body under the compressed Content-Type") } if !strings.Contains(err.Error(), "zlib") { t.Errorf("error does not name the decompression fault: %v", err) } if strings.Contains(err.Error(), "decode card") { t.Errorf("compressed body was retried as cards: %v", err) } } // TestDecodeEmptyBodyIsEmptyMessage pins that an empty body is an empty message // under either framing, since a zero-length reply carries no cards. Method: // decode a nil body under each Content-Type and assert no error and no cards. func TestDecodeEmptyBodyIsEmptyMessage(t *testing.T) { for _, ct := range []string{ContentTypeCompressed, ContentTypeUncompressed, ""} { msg, err := Decode(nil, ct) if err != nil { t.Fatalf("Decode(nil, %q): %v", ct, err) } if len(msg.Cards) != 0 { t.Errorf("Decode(nil, %q) returned %d cards, want 0", ct, len(msg.Cards)) } } } // TestDecodeReplyFramingRule pins §4's reply rule, which fossil's http.c // applies by case-insensitive prefix: no type is the compressed container, a // debug or uncompressed prefix is card text, another x-fossil prefix is the // container, and any other type is refused. Method: decode a container and // plain cards under each type and check which framing was chosen or that the // reply was refused. func TestDecodeReplyFramingRule(t *testing.T) { compressed := compressedContainer(t, 4096) plaintext := []byte("igot " + strings.Repeat("a", 40) + "\n") cases := []struct { contentType string body []byte ok bool }{ {"", compressed, true}, {ContentTypeCompressed, compressed, true}, {"APPLICATION/X-FOSSIL; x=1", compressed, true}, {ContentTypeDebug, plaintext, true}, {ContentTypeUncompressed + "; charset=utf-8", plaintext, true}, {"Application/X-Fossil-Debug", plaintext, true}, {"", plaintext, false}, {"text/html", plaintext, false}, {"text/plain", plaintext, false}, } for _, c := range cases { _, err := DecodeReply(c.body, c.contentType) if (err == nil) != c.ok { t.Errorf("DecodeReply(type %q): err = %v, want ok %v", c.contentType, err, c.ok) } } }