package gemini_test import ( "bytes" "errors" "io" "testing" sr "tildegit.org/tjp/sliderule" "tildegit.org/tjp/sliderule/gemini" ) func TestBuildResponses(t *testing.T) { table := []struct { name string response *sr.Response status sr.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 sr.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 := &sr.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 = &sr.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 *sr.Response) *sr.Response { other := &sr.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 *sr.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()) } }) } }