diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | gemini/request_test.go | 17 | ||||
-rw-r--r-- | gemini/response_test.go | 166 |
3 files changed, 184 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d83068 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +coverage.out diff --git a/gemini/request_test.go b/gemini/request_test.go index 1da24f7..c23d54b 100644 --- a/gemini/request_test.go +++ b/gemini/request_test.go @@ -3,6 +3,7 @@ package gemini_test import ( "bytes" "testing" + "net/url" "tildegit.org/tjp/gus/gemini" ) @@ -84,3 +85,19 @@ func TestParseRequest(t *testing.T) { }) } } + +func TestUnescapedQuery(t *testing.T) { + table := []string{ + "foo bar", + } + + for _, test := range table { + t.Run(test, func(t *testing.T) { + u, _ := url.Parse("gemini://domain.com/path?" + url.QueryEscape(test)) + result := gemini.Request{ URL: u }.UnescapedQuery() + if result != test { + t.Errorf("expected %q, got %q", test, result) + } + }) + } +} diff --git a/gemini/response_test.go b/gemini/response_test.go index 3e1f41f..7ffb585 100644 --- a/gemini/response_test.go +++ b/gemini/response_test.go @@ -149,3 +149,169 @@ func TestBuildResponses(t *testing.T) { }) } } + +func TestParseResponses(t *testing.T) { + table := []struct { + input string + status gemini.Status + meta string + body string + err error + }{ + { + input: "20 text/gemini\r\n# you got me!\n", + status: gemini.StatusSuccess, + meta: "text/gemini", + body: "# you got me!\n", + }, + { + input: "30 gemini://some.where/else\r\n", + status: gemini.StatusTemporaryRedirect, + meta: "gemini://some.where/else", + }, + { + input: "10 forgot the line ending", + err: gemini.InvalidResponseLineEnding, + }, + { + input: "10 wrong line ending\n", + err: gemini.InvalidResponseLineEnding, + }, + { + input: "10no space\r\n", + err: gemini.InvalidResponseHeaderLine, + }, + { + input: "no status code\r\n", + err: gemini.InvalidResponseHeaderLine, + }, + { + input: "31 gemini://domain.com/my/new/home\r\n", + status: gemini.StatusPermanentRedirect, + meta: "gemini://domain.com/my/new/home", + }, + } + + for _, test := range table { + t.Run(test.input, func(t *testing.T) { + response, err := gemini.ParseResponse(bytes.NewBufferString(test.input)) + + if !errors.Is(err, test.err) { + t.Fatalf("expected error %s, got %s", test.err, err) + } + + if err != nil { + return + } + + if response.Status != test.status { + t.Errorf("expected status %d, got %d", test.status, response.Status) + } + + if response.Meta != test.meta { + t.Errorf("expected meta %q, got %q", test.meta, response.Meta) + } + + if response.Body == nil { + if test.body != "" { + t.Errorf("expected body %q, got nil", test.body) + } + } else { + body, err := io.ReadAll(response.Body) + if err != nil { + t.Fatalf("error reading response body: %s", err.Error()) + } + + if test.body != string(body) { + t.Errorf("expected body %q, got %q", test.body, string(body)) + } + } + }) + } +} + +func TestResponseClose(t *testing.T) { + body := &rdCloser{Buffer: bytes.NewBufferString("the body here")} + resp := &gemini.Response{ + Status: gemini.StatusSuccess, + Meta: "text/gemini", + Body: body, + } + + if err := resp.Close(); err != nil { + t.Fatalf("response close error: %s", err.Error()) + } + + if !body.closed { + t.Error("response body was not closed by response.Close()") + } + + resp = &gemini.Response{ + Status: gemini.StatusInput, + Meta: "give me more", + } + + if err := resp.Close(); err != nil { + t.Fatalf("response close error: %s", err.Error()) + } +} + +type rdCloser struct { + *bytes.Buffer + closed bool +} + +func (rc *rdCloser) Close() error { + rc.closed = true + return nil +} + +func TestResponseWriteTo(t *testing.T) { + // invariant under test: WriteTo() sends the same bytes as Read() + + clone := func(resp *gemini.Response) *gemini.Response { + other := &gemini.Response{ + Status: resp.Status, + Meta: resp.Meta, + } + + if resp.Body != nil { + // the body could be one-time readable, so replace it with a buffer + buf, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("error reading response body: %s", err.Error()) + } + resp.Body = bytes.NewBuffer(buf) + + buf2 := make([]byte, len(buf)) + if copy(buf2, buf) != len(buf) { + t.Fatalf("short copy on a []byte") + } + + other.Body = bytes.NewBuffer(buf2) + } + + return resp + } + + r1 := &gemini.Response{ + Status: gemini.StatusSuccess, + Meta: "text/gemini", + Body: bytes.NewBufferString("the body goes here"), + } + r2 := clone(r1) + + wtbuf := &bytes.Buffer{} + if _, err := r1.WriteTo(wtbuf); err != nil { + t.Fatalf("response.WriteTo(): %s", err.Error()) + } + + rdbuf := make([]byte, wtbuf.Len()) + if n, err := r2.Read(rdbuf); err != nil { + t.Fatalf("response.Read() -> %d: %s", n, err.Error()) + } + + if wtbuf.String() != string(rdbuf) { + t.Fatalf("Read produced %q but WriteTo produced %q", string(rdbuf), wtbuf.String()) + } +} |