much more extensive gus/gemini testing
continuous-integration/drone/push Build is passing Details

This commit is contained in:
tjpcc 2023-01-11 13:19:42 -07:00
parent 4969e33e28
commit 029cd5b52d
8 changed files with 287 additions and 23 deletions

View File

@ -14,7 +14,7 @@ import (
// The only reason you might create more than one Client is to support separate TLS-cert
// driven identities.
//
// The zero value of Client is usable, it simply has no client TLS cert.
// The zero value is a usable Client with no client TLS certificate.
type Client struct {
tlsConf *tls.Config
}

View File

@ -0,0 +1,65 @@
package gemini_test
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"io"
"net/url"
"testing"
"tildegit.org/tjp/gus/gemini"
)
func TestRoundTrip(t *testing.T) {
tlsConf, err := gemini.FileTLS("./testdata/server.crt", "./testdata/server.key")
if err != nil {
t.Fatalf("FileTLS(): %s", err.Error())
}
handler := func(ctx context.Context, req *gemini.Request) *gemini.Response {
return gemini.Success("text/gemini", bytes.NewBufferString("you've found my page"))
}
server, err := gemini.NewServer(context.Background(), tlsConf, "tcp", "127.0.0.1:0", handler)
if err != nil {
t.Fatalf("NewServer(): %s", err.Error())
}
go server.Serve()
defer server.Close()
u, err := url.Parse(fmt.Sprintf("gemini://%s/test", server.Address()))
if err != nil {
t.Fatalf("url.Parse: %s", err.Error())
}
cli := gemini.NewClient(testClientTLS())
response, err := cli.RoundTrip(&gemini.Request{URL: u})
if err != nil {
t.Fatalf("RoundTrip(): %s", err.Error())
}
if response.Status != gemini.StatusSuccess {
t.Errorf("response status: expected %d, got %d", gemini.StatusSuccess, response.Status)
}
if response.Meta != "text/gemini" {
t.Errorf("response meta: expected \"text/gemini\", got %q", response.Meta)
}
if response.Body == nil {
t.Fatal("succcess response has nil body")
}
body, err := io.ReadAll(response.Body)
if err != nil {
t.Fatalf("ReadAll: %s", err.Error())
}
if string(body) != "you've found my page" {
t.Errorf("response body: expected \"you've found my page\", got %q", string(body))
}
}
func testClientTLS() *tls.Config {
return &tls.Config{InsecureSkipVerify: true}
}

117
gemini/handler_test.go Normal file
View File

@ -0,0 +1,117 @@
package gemini_test
import (
"bytes"
"context"
"io"
"net/url"
"strings"
"testing"
"tildegit.org/tjp/gus/gemini"
)
func TestFallthrough(t *testing.T) {
h1 := func(ctx context.Context, req *gemini.Request) *gemini.Response {
if req.Path == "/one" {
return gemini.Success("text/gemini", bytes.NewBufferString("one"))
}
return gemini.NotFound("nope")
}
h2 := func(ctx context.Context, req *gemini.Request) *gemini.Response {
if req.Path == "/two" {
return gemini.Success("text/gemini", bytes.NewBufferString("two"))
}
return gemini.NotFound("no way")
}
fth := gemini.FallthroughHandler(h1, h2)
u, err := url.Parse("gemini://test.local/one")
if err != nil {
t.Fatalf("url.Parse: %s", err.Error())
}
resp := fth(context.Background(), &gemini.Request{URL: u})
if resp.Status != gemini.StatusSuccess {
t.Errorf("expected status %d, got %d", gemini.StatusSuccess, resp.Status)
}
if resp.Meta != "text/gemini" {
t.Errorf(`expected meta "text/gemini", got %q`, resp.Meta)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Errorf("Read: %s", err.Error())
}
if string(body) != "one" {
t.Errorf(`expected body "one", got %q`, string(body))
}
u, err = url.Parse("gemini://test.local/two")
if err != nil {
t.Fatalf("url.Parse: %s", err.Error())
}
resp = fth(context.Background(), &gemini.Request{URL: u})
if resp.Status != gemini.StatusSuccess {
t.Errorf("expected status %d, got %d", gemini.StatusSuccess, resp.Status)
}
if resp.Meta != "text/gemini" {
t.Errorf(`expected meta "text/gemini", got %q`, resp.Meta)
}
body, err = io.ReadAll(resp.Body)
if err != nil {
t.Errorf("Read: %s", err.Error())
}
if string(body) != "two" {
t.Errorf(`expected body "two", got %q`, string(body))
}
u, err = url.Parse("gemini://test.local/three")
if err != nil {
t.Fatalf("url.Parse: %s", err.Error())
}
resp = fth(context.Background(), &gemini.Request{URL: u})
if resp.Status != gemini.StatusNotFound {
t.Errorf("expected status %d, got %d", gemini.StatusNotFound, resp.Status)
}
}
func TestFilter(t *testing.T) {
pred := func(ctx context.Context, req *gemini.Request) bool {
return strings.HasPrefix(req.Path, "/allow")
}
base := func(ctx context.Context, req *gemini.Request) *gemini.Response {
return gemini.Success("text/gemini", bytes.NewBufferString("allowed!"))
}
handler := gemini.Filter(pred, base, nil)
u, err := url.Parse("gemini://test.local/allow/please")
if err != nil {
t.Fatalf("url.Parse: %s", err.Error())
}
resp := handler(context.Background(), &gemini.Request{URL: u})
if resp.Status != gemini.StatusSuccess {
t.Errorf("expected status %d, got %d", gemini.StatusSuccess, resp.Status)
}
u, err = url.Parse("gemini://test.local/disallow/please")
if err != nil {
t.Fatalf("url.Parse: %s", err.Error())
}
resp = handler(context.Background(), &gemini.Request{URL: u})
if resp.Status != gemini.StatusNotFound {
t.Errorf("expected status %d, got %d", gemini.StatusNotFound, resp.Status)
}
}

View File

@ -248,7 +248,7 @@ func TestResponseClose(t *testing.T) {
resp = &gemini.Response{
Status: gemini.StatusInput,
Meta: "give me more",
Meta: "give me more",
}
if err := resp.Close(); err != nil {
@ -272,7 +272,7 @@ func TestResponseWriteTo(t *testing.T) {
clone := func(resp *gemini.Response) *gemini.Response {
other := &gemini.Response{
Status: resp.Status,
Meta: resp.Meta,
Meta: resp.Meta,
}
if resp.Body != nil {
@ -291,27 +291,44 @@ func TestResponseWriteTo(t *testing.T) {
other.Body = bytes.NewBuffer(buf2)
}
return resp
return other
}
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())
table := []struct {
name string
response *gemini.Response
}{
{
name: "simple success",
response: gemini.Success(
"text/gemini",
bytes.NewBufferString("the body goes here"),
),
},
{
name: "no body",
response: gemini.Input("need more pls"),
},
}
rdbuf := make([]byte, wtbuf.Len())
if n, err := r2.Read(rdbuf); err != nil {
t.Fatalf("response.Read() -> %d: %s", n, err.Error())
}
for _, test := range table {
t.Run(test.name, func(t *testing.T) {
r1 := test.response
r2 := clone(test.response)
if wtbuf.String() != string(rdbuf) {
t.Fatalf("Read produced %q but WriteTo produced %q", string(rdbuf), wtbuf.String())
rdbuf, err := io.ReadAll(r1)
if err != nil {
t.Fatalf("response.Read(): %s", err.Error())
}
wtbuf := &bytes.Buffer{}
if _, err := 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())
}
})
}
}

View File

@ -32,10 +32,12 @@ func NewServer(
return nil, err
}
addr := listener.Addr()
s := &Server{
ctx: ctx,
network: network,
address: address,
network: addr.Network(),
address: addr.String(),
wg: &sync.WaitGroup{},
listener: tls.NewListener(listener, tlsConfig),
handler: handler,
@ -59,7 +61,7 @@ func (s *Server) Serve() error {
s.ctx, s.cancel = context.WithCancel(s.ctx)
s.wg.Add(1)
go s.propagateCancel()
s.propagateCancel()
for {
conn, err := s.listener.Accept()

18
gemini/testdata/server.crt vendored Normal file
View File

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC7jCCAdYCAQcwDQYJKoZIhvcNAQELBQAwPTESMBAGA1UEAwwJbG9jYWxob3N0
MQswCQYDVQQGEwJVUzEaMBgGA1UEBwwRU2FuIEZyYW5jaXNjbywgQ0EwHhcNMjMw
MTExMjAwMDU5WhcNMjUwNDE1MjAwMDU5WjA9MRIwEAYDVQQDDAlsb2NhbGhvc3Qx
CzAJBgNVBAYTAlVTMRowGAYDVQQHDBFTYW4gRnJhbmNpc2NvLCBDQTCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBALlaPa1AxDQnMo0qQxY5/Bf7MNf1x6tN
xjkpMnQnPM+cHmmlkEhI1zwLk/LrLxwq7+OOxMTPrJglrAiDAp1uCZHjKcTMFnwO
9M5vf8LjtYBjZd8+OSHyYV37gxw7h9/Wsxl+1Yw40QaJKM9auj2xOyaDj5Ou9+yp
CfbGSpVUTnqReOVFg2QSNwEviOZu1SvAouPyO98WKoXjn7K5mxE545e4mgF1EMht
jB5kH6kXqZSUszlGA1MkX3AlDsYJIcYnDwelNvw6XTPpkT2wNehxPyD0iP4rs+W4
5hgV8wYokpgrM3xxe0c4mop5bzrp2Hyz3WxnF7KwtJgHW/6YxhG73skCAwEAATAN
BgkqhkiG9w0BAQsFAAOCAQEAfI+UE/3d0Fb8BZ2gtv1kUh8yx75LUbpg1aOEsZdP
Rji+GkL5xiFDsm7BwqTKziAjDtjL2qtGcJJ835shsGiUSK6qJuf9C944utUvCoFm
b4aUZ8fTmN7PkwRS61nIcHaS1zkiFzUdvbquV3QWSnl9kC+yDLHT0Z535tcvCMVM
bO7JMj1sxml4Y9B/hfY7zAZJt1giSNH1iDeX2pTpmPPI40UsRn98cC8HZ0d8wFrv
yc3hKkz8E+WTgZUf7jFk/KX/T5uwu+Y85emwfbb82KIR3oqhkJIfOfpqop2duZXB
hMuO1QWEBkZ/hpfrAsN/foz8v46P9qgW8gfOfzhyBcqLvA==
-----END CERTIFICATE-----

18
gemini/testdata/server.csr vendored Normal file
View File

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIC0zCCAbsCAQAwPTESMBAGA1UEAwwJbG9jYWxob3N0MQswCQYDVQQGEwJVUzEa
MBgGA1UEBwwRU2FuIEZyYW5jaXNjbywgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQC5Wj2tQMQ0JzKNKkMWOfwX+zDX9cerTcY5KTJ0JzzPnB5ppZBI
SNc8C5Py6y8cKu/jjsTEz6yYJawIgwKdbgmR4ynEzBZ8DvTOb3/C47WAY2XfPjkh
8mFd+4McO4ff1rMZftWMONEGiSjPWro9sTsmg4+TrvfsqQn2xkqVVE56kXjlRYNk
EjcBL4jmbtUrwKLj8jvfFiqF45+yuZsROeOXuJoBdRDIbYweZB+pF6mUlLM5RgNT
JF9wJQ7GCSHGJw8HpTb8Ol0z6ZE9sDXocT8g9Ij+K7PluOYYFfMGKJKYKzN8cXtH
OJqKeW866dh8s91sZxeysLSYB1v+mMYRu97JAgMBAAGgUTBPBgkqhkiG9w0BCQ4x
QjBAMDEGA1UdJQQqMCgGCCsGAQUFBwMBBggrBgEFBQcDAgYIKwYBBQUHAwMGCCsG
AQUFBwMEMAsGA1UdDwQEAwIFIDANBgkqhkiG9w0BAQsFAAOCAQEAOKb0Mnnm7oLT
0fz7+CQ4KYva/dmr75k38PPRXGs/7Ls6nhu59yNhudHJtRyjaAzffwfg1NWxKlUV
gDf+4K6S+cjz6bWVdU4XwH37V01GWWgzmwDGEsoZZpNstuq87BhI62BKQFKqJrw2
pqNYoM+p4K7OnOUNT60LshzThguMb4h53YcTXyv7wAf9LABc4v0daVErunDZ5Elh
QwlUZT/pngTLJiXDjrWB3PGnniTbC0OYhKKmFbX/dIR/TlUH7Fcc4mE9f514mU0n
zys/mc57gBTdI11oIw1fkQJ6f3LDk3MsFfJntwhxjVeSXJUNOBwsxmxdyigjsifY
J+SpNczO1Q==
-----END CERTIFICATE REQUEST-----

27
gemini/testdata/server.key vendored Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAuVo9rUDENCcyjSpDFjn8F/sw1/XHq03GOSkydCc8z5weaaWQ
SEjXPAuT8usvHCrv447ExM+smCWsCIMCnW4JkeMpxMwWfA70zm9/wuO1gGNl3z45
IfJhXfuDHDuH39azGX7VjDjRBokoz1q6PbE7JoOPk6737KkJ9sZKlVROepF45UWD
ZBI3AS+I5m7VK8Ci4/I73xYqheOfsrmbETnjl7iaAXUQyG2MHmQfqReplJSzOUYD
UyRfcCUOxgkhxicPB6U2/DpdM+mRPbA16HE/IPSI/iuz5bjmGBXzBiiSmCszfHF7
RziainlvOunYfLPdbGcXsrC0mAdb/pjGEbveyQIDAQABAoIBAQC36ylkLu4Bahup
I5RqC6NwEFpJEKLOAmB8+7oKs5yNzTYIUra2Y0DfXgWyd1fJtXlP7aymNgPm/QqV
b5o6qKNqVWRu2Kw+8YBNDypRMi45dWfyewWp/55J6XYRn6iVna8dz1MKzp3qxFLw
XfCLor802jqvqmBsPteaPOxo/LzatKhXp/mcO/hsxeMr1iSUVHTrQEIU/aIkmAqT
/eXp/zVZk7O9Tx8wwCijB3v7j3zTEkcKSwFlAp0w01XeqllmqA5P9rW3vVGXJVIM
t6t9C8XcJWPIOURz3JWZJpUBSZsyNe2N/wbCgkQV81A0s+4praKzgDbjE+njb0C/
1CClbHV5AoGBAO/mnOzHe7ZJyYfuiu6ZR2REBY61n2J6DkL1stkN5xd+Op25afHT
jLBjU98hM/AMtP1aHWFQpdEe0uyqRjV6PbpNE8j/m9AVfjZxzwR4ITW2xqUhXOSz
89o832RO54TTr19YGnIhdU8dDQmYOcKmCSuw6KwCfHwBzkFuDFZGk/4/AoGBAMXK
gzNyX3tN9Ug5AUo/Az4jQRSoyLjfnce0a0TF4jxEacUBx2COq3zaV/VADEFBla1t
5roOAUyJ3V6fXtZnoqwZPYh6iGP8p7Tj6vyXI4SDktV0uAV57qSdajqxTrA7yoXr
zrbxv3U/3vXr3JTsP42U5zp1m5n1VfVqCXBkynD3AoGBAOvs7JjDWXuctzASPNmH
LjmB18FQBk3vYQUi4l8pmAF3pyejx3gGJw70r+/4lD5YEMozjD8+88Njv+T1U5SW
Agysbm+2SMJr0LK0W/W2Olq7xEFzPQrBmmgeg0b/fhoXoBlw6JkjJF3IYSD1bqBp
bw1jrn4y979weynHkyRpxnM7AoGBALGSzRPlPR/gr7P1qdjUlb61u/omRn7kFC11
J1EJL8HX0fXTUQK5U/C1vn4q0FXN4elgX+LuK/BhXeNTxbtMM9m6l2nuSIEsFgzr
Cs9XicWwsqT9MzGHdN9JjFPBV9oU9BAj0uSgSbmkbDHxXYo+SBh+dNIhQF+KyW+Z
kXvcoXulAoGAA2hnEA17nJ7Vj1DZ4CoRblgjZFAMB64slcSesaorp3WWehvaXO8u
jbvWuvj58DgvTLiv8xPIn4Zsjd0a77ysifvUcmxSRa/k9UIle/lwjmXGjQ1GSMEI
FB5ZTqjLZwS9Y5BDxlPcYF7vqE9fNpcxmcfHGmSF5YAHvFOfGH6B63M=
-----END RSA PRIVATE KEY-----