protocol_test.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. package localapi
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "net"
  7. "strings"
  8. "testing"
  9. "github.com/stretchr/testify/assert"
  10. "github.com/stretchr/testify/require"
  11. )
  12. // TestReadFrameLimitIsOnTheBody pins the framing rule at its edge: the limit bounds the
  13. // JSON body, so the newline that terminates the frame must not count against it.
  14. func TestReadFrameLimitIsOnTheBody(t *testing.T) {
  15. // A JSON string whose encoded form is exactly n bytes, with or without a terminator.
  16. body := func(n int) string {
  17. return fmt.Sprintf(`"%s"`, strings.Repeat("a", n-2))
  18. }
  19. var v string
  20. // Exactly at the limit, terminated: accepted.
  21. v = ""
  22. require.NoError(t, readFrame(bytes.NewReader([]byte(body(64)+"\n")), 64, "request", &v, true))
  23. assert.Len(t, v, 62)
  24. // Exactly at the limit, EOF instead of a newline: still accepted.
  25. v = ""
  26. require.NoError(t, readFrame(bytes.NewReader([]byte(body(64))), 64, "request", &v, true))
  27. assert.Len(t, v, 62)
  28. // One byte over: refused as oversized, not as a parse failure.
  29. err := readFrame(bytes.NewReader([]byte(body(65)+"\n")), 64, "request", &v, true)
  30. require.Error(t, err)
  31. assert.Contains(t, err.Error(), "exceeded 64 bytes")
  32. }
  33. // stallingReader hands out its bytes and then fails with err, the shape of a peer that
  34. // sends part of a frame and then stops, until the deadline on the connection lands.
  35. type stallingReader struct {
  36. data []byte
  37. err error
  38. }
  39. func (r *stallingReader) Read(p []byte) (int, error) {
  40. if len(r.data) == 0 {
  41. return 0, r.err
  42. }
  43. n := copy(p, r.data)
  44. r.data = r.data[n:]
  45. return n, nil
  46. }
  47. type timeoutError struct{}
  48. func (timeoutError) Error() string { return "i/o timeout" }
  49. func (timeoutError) Timeout() bool { return true }
  50. func (timeoutError) Temporary() bool { return true }
  51. // TestReadFramePartialLineThenReadErrorIsTheError: a read that fails after part of a
  52. // frame has arrived is a failed read, and the caller sees that error. The old behaviour
  53. // treated the fragment as the whole frame and reported a parse failure, so a client
  54. // that stalled mid request was told its JSON was malformed, and a daemon that stalled
  55. // mid response hid the timeout the client's error path looks for.
  56. func TestReadFramePartialLineThenReadErrorIsTheError(t *testing.T) {
  57. var v map[string]any
  58. err := readFrame(&stallingReader{data: []byte(`{"command": "hel`), err: timeoutError{}}, 1024, "request", &v, true)
  59. require.Error(t, err)
  60. var ne net.Error
  61. require.ErrorAs(t, err, &ne, "the timeout is still visible through the wrapping: %v", err)
  62. assert.True(t, ne.Timeout())
  63. assert.NotContains(t, err.Error(), "parse", "a stalled peer is not a parse failure")
  64. // EOF after an unterminated line is the one exception: a final line with no newline
  65. // is still a frame.
  66. v = nil
  67. require.NoError(t, readFrame(&stallingReader{data: []byte(`{"command": "help"}`), err: io.EOF}, 1024, "request", &v, true))
  68. assert.Equal(t, "help", v["command"])
  69. // And an unterminated line that is EOF only because the limit cut it off is still
  70. // reported as oversized.
  71. err = readFrame(bytes.NewReader([]byte(strings.Repeat("a", 70)+"\n")), 64, "request", &v, true)
  72. require.Error(t, err)
  73. assert.Contains(t, err.Error(), "exceeded 64 bytes")
  74. }
  75. // TestReadFrameUnknownFieldIsRefusedWhenStrict: the daemon refuses a request field it
  76. // does not know, as it refuses an unknown parameter, so a misspelling is reported rather
  77. // than ignored; the client, reading leniently, passes over a field a newer daemon sends.
  78. func TestReadFrameUnknownFieldIsRefusedWhenStrict(t *testing.T) {
  79. var req Request
  80. err := readFrame(bytes.NewReader([]byte(`{"version":1,"comand":"help"}`+"\n")), 1024, "request", &req, true)
  81. require.ErrorContains(t, err, `unknown field "comand"`)
  82. var res Response
  83. require.NoError(t, readFrame(bytes.NewReader([]byte(`{"version":1,"later":true}`+"\n")), 1024, "response", &res, false))
  84. assert.Equal(t, 1, res.Version)
  85. }