From 029cd5b52db8385d221690cbf75e5e4825e145d8 Mon Sep 17 00:00:00 2001 From: tjpcc Date: Wed, 11 Jan 2023 13:19:42 -0700 Subject: much more extensive gus/gemini testing --- gemini/client.go | 2 +- gemini/client_server_test.go | 65 ++++++++++++++++++++++++ gemini/handler_test.go | 117 +++++++++++++++++++++++++++++++++++++++++++ gemini/response_test.go | 53 +++++++++++++------- gemini/serve.go | 8 +-- gemini/testdata/server.crt | 18 +++++++ gemini/testdata/server.csr | 18 +++++++ gemini/testdata/server.key | 27 ++++++++++ 8 files changed, 286 insertions(+), 22 deletions(-) create mode 100644 gemini/client_server_test.go create mode 100644 gemini/handler_test.go create mode 100644 gemini/testdata/server.crt create mode 100644 gemini/testdata/server.csr create mode 100644 gemini/testdata/server.key (limited to 'gemini') diff --git a/gemini/client.go b/gemini/client.go index aca4576..05ab8cd 100644 --- a/gemini/client.go +++ b/gemini/client.go @@ -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 } diff --git a/gemini/client_server_test.go b/gemini/client_server_test.go new file mode 100644 index 0000000..5dd61f1 --- /dev/null +++ b/gemini/client_server_test.go @@ -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} +} diff --git a/gemini/handler_test.go b/gemini/handler_test.go new file mode 100644 index 0000000..c83df65 --- /dev/null +++ b/gemini/handler_test.go @@ -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) + } +} diff --git a/gemini/response_test.go b/gemini/response_test.go index 7ffb585..616fac4 100644 --- a/gemini/response_test.go +++ b/gemini/response_test.go @@ -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"), + 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"), + }, } - r2 := clone(r1) - wtbuf := &bytes.Buffer{} - if _, err := r1.WriteTo(wtbuf); err != nil { - t.Fatalf("response.WriteTo(): %s", err.Error()) - } + for _, test := range table { + t.Run(test.name, func(t *testing.T) { + r1 := test.response + r2 := clone(test.response) - rdbuf := make([]byte, wtbuf.Len()) - if n, err := r2.Read(rdbuf); err != nil { - t.Fatalf("response.Read() -> %d: %s", n, err.Error()) - } + rdbuf, err := io.ReadAll(r1) + if err != nil { + t.Fatalf("response.Read(): %s", err.Error()) + } - if wtbuf.String() != string(rdbuf) { - t.Fatalf("Read produced %q but WriteTo produced %q", string(rdbuf), wtbuf.String()) + 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()) + } + }) } } diff --git a/gemini/serve.go b/gemini/serve.go index f9a8a1c..bc13531 100644 --- a/gemini/serve.go +++ b/gemini/serve.go @@ -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() diff --git a/gemini/testdata/server.crt b/gemini/testdata/server.crt new file mode 100644 index 0000000..4e3dc74 --- /dev/null +++ b/gemini/testdata/server.crt @@ -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----- diff --git a/gemini/testdata/server.csr b/gemini/testdata/server.csr new file mode 100644 index 0000000..40934c2 --- /dev/null +++ b/gemini/testdata/server.csr @@ -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----- diff --git a/gemini/testdata/server.key b/gemini/testdata/server.key new file mode 100644 index 0000000..44ce348 --- /dev/null +++ b/gemini/testdata/server.key @@ -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----- -- cgit v1.2.3