336 lines
7.8 KiB
Go
336 lines
7.8 KiB
Go
package gemini_test
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"testing"
|
|
|
|
"tildegit.org/tjp/gus"
|
|
"tildegit.org/tjp/gus/gemini"
|
|
)
|
|
|
|
func TestBuildResponses(t *testing.T) {
|
|
table := []struct {
|
|
name string
|
|
response *gus.Response
|
|
status gus.Status
|
|
meta string
|
|
body string
|
|
}{
|
|
{
|
|
name: "input response",
|
|
response: gemini.Input("prompt here"),
|
|
status: gemini.StatusInput,
|
|
meta: "prompt here",
|
|
},
|
|
{
|
|
name: "sensitive input response",
|
|
response: gemini.SensitiveInput("password please"),
|
|
status: gemini.StatusSensitiveInput,
|
|
meta: "password please",
|
|
},
|
|
{
|
|
name: "success response",
|
|
response: gemini.Success("text/gemini", bytes.NewBufferString("body text here")),
|
|
status: gemini.StatusSuccess,
|
|
meta: "text/gemini",
|
|
body: "body text here",
|
|
},
|
|
{
|
|
name: "temporary redirect",
|
|
response: gemini.Redirect("/foo/bar"),
|
|
status: gemini.StatusTemporaryRedirect,
|
|
meta: "/foo/bar",
|
|
},
|
|
{
|
|
name: "permanent redirect",
|
|
response: gemini.PermanentRedirect("/baz/qux"),
|
|
status: gemini.StatusPermanentRedirect,
|
|
meta: "/baz/qux",
|
|
},
|
|
{
|
|
name: "fail response",
|
|
response: gemini.Failure(errors.New("a failure")),
|
|
status: gemini.StatusTemporaryFailure,
|
|
meta: "a failure",
|
|
},
|
|
{
|
|
name: "server unavailable",
|
|
response: gemini.Unavailable("server unavailable"),
|
|
status: gemini.StatusServerUnavailable,
|
|
meta: "server unavailable",
|
|
},
|
|
{
|
|
name: "cgi error",
|
|
response: gemini.CGIError("some cgi error msg"),
|
|
status: gemini.StatusCGIError,
|
|
meta: "some cgi error msg",
|
|
},
|
|
{
|
|
name: "proxy error",
|
|
response: gemini.ProxyError("upstream's full"),
|
|
status: gemini.StatusProxyError,
|
|
meta: "upstream's full",
|
|
},
|
|
{
|
|
name: "rate limiting",
|
|
response: gemini.SlowDown(15),
|
|
status: gemini.StatusSlowDown,
|
|
meta: "15",
|
|
},
|
|
{
|
|
name: "permanent failure",
|
|
response: gemini.PermanentFailure(errors.New("wut r u doin")),
|
|
status: gemini.StatusPermanentFailure,
|
|
meta: "wut r u doin",
|
|
},
|
|
{
|
|
name: "not found",
|
|
response: gemini.NotFound("nope"),
|
|
status: gemini.StatusNotFound,
|
|
meta: "nope",
|
|
},
|
|
{
|
|
name: "gone",
|
|
response: gemini.Gone("all out of that"),
|
|
status: gemini.StatusGone,
|
|
meta: "all out of that",
|
|
},
|
|
{
|
|
name: "refuse proxy",
|
|
response: gemini.RefuseProxy("no I don't think I will"),
|
|
status: gemini.StatusProxyRequestRefused,
|
|
meta: "no I don't think I will",
|
|
},
|
|
{
|
|
name: "bad request",
|
|
response: gemini.BadRequest("that don't make no sense"),
|
|
status: gemini.StatusBadRequest,
|
|
meta: "that don't make no sense",
|
|
},
|
|
{
|
|
name: "require cert",
|
|
response: gemini.RequireCert("cert required"),
|
|
status: gemini.StatusClientCertificateRequired,
|
|
meta: "cert required",
|
|
},
|
|
{
|
|
name: "cert auth failure",
|
|
response: gemini.CertAuthFailure("you can't see that"),
|
|
status: gemini.StatusCertificateNotAuthorized,
|
|
meta: "you can't see that",
|
|
},
|
|
{
|
|
name: "invalid cert",
|
|
response: gemini.CertInvalid("bad cert dude"),
|
|
status: gemini.StatusCertificateNotValid,
|
|
meta: "bad cert dude",
|
|
},
|
|
}
|
|
|
|
for _, test := range table {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if test.response.Status != test.status {
|
|
t.Errorf("expected status %d, got %d", test.status, test.response.Status)
|
|
}
|
|
if test.response.Meta != test.meta {
|
|
t.Errorf("expected meta %q, got %q", test.meta, test.response.Meta)
|
|
}
|
|
|
|
responseBytes, err := io.ReadAll(gemini.NewResponseReader(test.response))
|
|
if err != nil {
|
|
t.Fatalf("error reading response body: %q", err.Error())
|
|
}
|
|
|
|
body := string(bytes.SplitN(responseBytes, []byte("\r\n"), 2)[1])
|
|
if body != test.body {
|
|
t.Errorf("expected body %q, got %q", test.body, body)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseResponses(t *testing.T) {
|
|
table := []struct {
|
|
input string
|
|
status gus.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 := &gus.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 = &gus.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 *gus.Response) *gus.Response {
|
|
other := &gus.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 other
|
|
}
|
|
|
|
table := []struct {
|
|
name string
|
|
response *gus.Response
|
|
}{
|
|
{
|
|
name: "simple success",
|
|
response: gemini.Success(
|
|
"text/gemini",
|
|
bytes.NewBufferString("the body goes here"),
|
|
),
|
|
},
|
|
{
|
|
name: "no body",
|
|
response: gemini.Input("need more pls"),
|
|
},
|
|
}
|
|
|
|
for _, test := range table {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
r1 := test.response
|
|
r2 := clone(test.response)
|
|
|
|
rdbuf, err := io.ReadAll(gemini.NewResponseReader(r1))
|
|
if err != nil {
|
|
t.Fatalf("response.Read(): %s", err.Error())
|
|
}
|
|
|
|
wtbuf := &bytes.Buffer{}
|
|
if _, err := gemini.NewResponseReader(r2).WriteTo(wtbuf); err != nil {
|
|
t.Fatalf("response.WriteTo(): %s", err.Error())
|
|
}
|
|
|
|
if wtbuf.String() != string(rdbuf) {
|
|
t.Fatalf("Read produced %q but WriteTo produced %q", string(rdbuf), wtbuf.String())
|
|
}
|
|
})
|
|
}
|
|
}
|