package gig
import (
"bytes"
"crypto/tls"
"errors"
"io/ioutil"
"net"
"strings"
"testing"
"time"
"github.com/matryer/is"
)
func TestGig(t *testing.T) {
is := is.New(t)
g := New()
c, conn := g.NewFakeContext("/", nil)
// Router
is.True(g.router != nil)
// DefaultGeminiErrorHandler
DefaultGeminiErrorHandler(errors.New("error"), c)
is.Equal("50 error\r\n", conn.Written)
}
func TestGigStatic(t *testing.T) {
is := is.New(t)
g := New()
// OK
g.Static("/images_ok", "_fixture/images")
b := request("/images_ok/walle.png", g)
is.True(strings.HasPrefix(b, "20 image/png\r\n"))
// Empty root
g.Static("/empty_root", "")
b = request("/empty_root/_fixture/images/walle.png", g)
is.True(strings.HasPrefix(b, "20 image/png\r\n"))
// Missing file
g.Static("/images_none", "_fixture/missing")
b = request("/images_none/", g)
is.Equal("51 Not Found\r\n", b)
b = request("/images_none/walle.png", g)
is.Equal("51 Not Found\r\n", b)
// Directory Listing
g.Static("/dir_no_index", "_fixture/folder")
b = request("/dir_no_index/", g)
is.Equal("20 text/gemini\r\n# Listing /dir_no_index/\n\n=> /dir_no_index/about.gmi about.gmi [ 29B ]\n=> /dir_no_index/another.blah another.blah [ 14B ]\n", b)
// Directory Listing with index.gmi
g.Static("/dir", "_fixture")
b = request("/dir/", g)
is.Equal("20 text/gemini\r\n# Hello from gig\n\n=> / 🏠 Home\n", b)
b = request("/dir/folder", g)
is.Equal("20 text/gemini\r\n# Listing /dir/folder\n\n=> /dir/folder/about.gmi about.gmi [ 29B ]\n=> /dir/folder/another.blah another.blah [ 14B ]\n", b)
// File without known mime
b = request("/dir/folder/another.blah", g)
is.Equal("20 octet/stream\r\n# Another page", b)
// Escape
b = request("/dir/../../../../../../../../etc/profile", g)
is.Equal(b, "51 Not Found\r\n")
}
func TestGigFile(t *testing.T) {
is := is.New(t)
g := New()
g.File("/walle", "_fixture/images/walle.png")
b := request("/walle", g)
is.True(strings.HasPrefix(b, "20 "))
g.File("/missing", "_fixture/images/johnny.png")
b = request("/missing", g)
is.Equal(b, "51 Not Found\r\n")
}
func TestGigMiddleware(t *testing.T) {
is := is.New(t)
g := New()
buf := new(bytes.Buffer)
g.Pre(func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
is.True(c.Path() == "")
buf.WriteString("-1")
return next(c)
}
})
g.Use(func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
buf.WriteString("1")
return next(c)
}
})
g.Use(func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
buf.WriteString("2")
return next(c)
}
})
g.Use(func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
buf.WriteString("3")
return next(c)
}
})
// Route
g.Handle("/", func(c Context) error {
return c.Text("OK")
})
b := request("/", g)
is.Equal("-1123", buf.String())
is.Equal("20 text/plain\r\nOK", b)
}
func TestGigMiddlewareError(t *testing.T) {
is := is.New(t)
g := New()
g.Use(func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
return NewErrorFrom(ErrPermanentFailure, "oops")
}
})
g.Handle("/", NotFoundHandler)
b := request("/", g)
is.Equal("50 oops\r\n", b)
}
func TestGigHandler(t *testing.T) {
is := is.New(t)
g := New()
// HandlerFunc
g.Handle("/ok", func(c Context) error {
return c.Text("OK")
})
b := request("/ok", g)
is.Equal("20 text/plain\r\nOK", b)
}
func TestGigHandle(t *testing.T) {
is := is.New(t)
g := New()
g.Handle("/", func(c Context) error {
return c.Text("hello")
})
b := request("/", g)
is.Equal("20 text/plain\r\nhello", b)
}
func TestGigURL(t *testing.T) {
is := is.New(t)
g := New()
static := func(Context) error { return nil }
getUser := func(Context) error { return nil }
getFile := func(Context) error { return nil }
g.Handle("/static/file", static)
g.Handle("/users/:id", getUser)
gr := g.Group("/group")
gr.Handle("/users/:uid/files/:fid", getFile)
is.Equal("/static/file", g.URL(static))
is.Equal("/users/:id", g.URL(getUser))
is.Equal("/users/1", g.URL(getUser, "1"))
is.Equal("/group/users/1/files/:fid", g.URL(getFile, "1"))
is.Equal("/group/users/1/files/1", g.URL(getFile, "1", "1"))
}
func TestGigRoutes(t *testing.T) {
is := is.New(t)
g := New()
routes := []*Route{
{"/users/:user/events", ""},
{"/users/:user/events/public", ""},
{"/repos/:owner/:repo/git/refs", ""},
{"/repos/:owner/:repo/git/tags", ""},
}
for _, r := range routes {
g.Handle(r.Path, func(c Context) error {
return c.Text("OK")
})
}
is.Equal(len(routes), len(g.Routes()))
for _, r := range g.Routes() {
found := false
for _, rr := range routes {
if r.Path == rr.Path {
found = true
break
}
}
if !found {
t.Errorf("Route %s not found", r.Path)
}
}
}
func TestGigEncodedPath(t *testing.T) {
is := is.New(t)
g := New()
g.Handle("/:id", func(c Context) error {
return c.NoContent(StatusInput, "please enter name")
})
c, conn := g.NewFakeContext("/with%2Fslash", nil)
g.ServeGemini(c)
is.Equal("10 please enter name\r\n", conn.Written)
}
func TestGigGroup(t *testing.T) {
is := is.New(t)
g := New()
buf := new(bytes.Buffer)
g.Use(MiddlewareFunc(func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
buf.WriteString("0")
return next(c)
}
}))
h := func(c Context) error {
return c.NoContent(StatusInput, "please enter name")
}
//--------
// Routes
//--------
g.Handle("/users", h)
// Group
g1 := g.Group("/group1")
g1.Use(func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
buf.WriteString("1")
return next(c)
}
})
g1.Handle("", h)
// Nested groups with middleware
g2 := g.Group("/group2")
g2.Use(func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
buf.WriteString("2")
return next(c)
}
})
g3 := g2.Group("/group3")
g3.Use(func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
buf.WriteString("3")
return next(c)
}
})
g3.Handle("", h)
request("/users", g)
is.Equal("0", buf.String())
buf.Reset()
request("/group1", g)
is.Equal("01", buf.String())
buf.Reset()
request("/group2/group3", g)
is.Equal("023", buf.String())
}
func TestGigNotFound(t *testing.T) {
is := is.New(t)
g := New()
c, conn := g.NewFakeContext("/files", nil)
g.ServeGemini(c)
is.Equal("51 Not Found\r\n", conn.Written)
}
func TestGigServeGemini(t *testing.T) {
var (
is = is.New(t)
g1 = New()
g2 = New()
ctx, conn = g1.NewFakeContext("/files", nil)
)
g2.Handle("/", func(c Context) error {
is.True(c.Gig() == g2)
is.True(c != ctx)
return c.NoContent(StatusSuccess, "ok")
})
g2.ServeGemini(ctx)
is.Equal("51 Not Found\r\n", conn.Written)
}
func TestGigRun(t *testing.T) {
g := New()
go func() {
_ = g.Run("127.0.0.1:0", "_fixture/certs/cert.pem", "_fixture/certs/key.pem")
}()
time.Sleep(200 * time.Millisecond)
g.Close()
}
func TestGigRun_BadAddress(t *testing.T) {
is := is.New(t)
g := New()
err := g.Run("garbage address", "_fixture/certs/cert.pem", "_fixture/certs/key.pem")
is.True(err != nil)
is.True(strings.Contains(err.Error(), "address garbage address: missing port in address"))
}
func TestGigRunByteString(t *testing.T) {
is := is.New(t)
cert, err := ioutil.ReadFile("_fixture/certs/cert.pem")
is.NoErr(err)
key, err := ioutil.ReadFile("_fixture/certs/key.pem")
is.NoErr(err)
switchedCertError := errors.New("tls: failed to find certificate PEM data in certificate input, but did find a private key; PEM inputs may have been switched")
testCases := []struct {
cert interface{}
key interface{}
expectedErr error
name string
}{
{
cert: "_fixture/certs/cert.pem",
key: "_fixture/certs/key.pem",
expectedErr: nil,
name: `ValidCertAndKeyFilePath`,
},
{
cert: cert,
key: key,
expectedErr: nil,
name: `ValidCertAndKeyByteString`,
},
{
cert: cert,
key: 1,
expectedErr: ErrInvalidCertOrKeyType,
name: `InvalidKeyType`,
},
{
cert: 0,
key: key,
expectedErr: ErrInvalidCertOrKeyType,
name: `InvalidCertType`,
},
{
cert: 0,
key: 1,
expectedErr: ErrInvalidCertOrKeyType,
name: `InvalidCertAndKeyTypes`,
},
{
cert: "_fixture/certs/key.pem",
key: "_fixture/certs/cert.pem",
expectedErr: switchedCertError,
name: `BadCertAndKey`,
},
}
for _, test := range testCases {
test := test
t.Run(test.name, func(t *testing.T) {
is := is.New(t)
g := New()
g.HideBanner = true
go func() {
err := g.Run("127.0.0.1:0", test.cert, test.key)
if test.expectedErr != nil {
is.Equal(err.Error(), test.expectedErr.Error())
} else if err != ErrServerClosed { // Prevent the test to fail after closing the servers
is.NoErr(err)
}
}()
time.Sleep(200 * time.Millisecond)
g.Close()
})
}
}
func request(path string, g *Gig) string {
c, conn := g.NewFakeContext(path, nil)
g.ServeGemini(c)
return conn.Written
}
func TestGeminiError(t *testing.T) {
is := is.New(t)
t.Run("manual", func(t *testing.T) {
err := NewError(StatusSlowDown, "oops")
is.Equal("error=oops", err.Error())
})
t.Run("existing", func(t *testing.T) {
err := ErrSlowDown
is.Equal("error=Slow Down", err.Error())
})
t.Run("inherited", func(t *testing.T) {
err := NewErrorFrom(ErrSlowDown, "oops")
is.Equal("error=oops", err.Error())
})
}
func TestGigClose(t *testing.T) {
is := is.New(t)
g := New()
errCh := make(chan error)
go func() {
errCh <- g.Run("127.0.0.1:0", "_fixture/certs/cert.pem", "_fixture/certs/key.pem")
}()
time.Sleep(200 * time.Millisecond)
if err := g.Close(); err != nil {
t.Fatal(err)
}
is.True(strings.Contains(g.Close().Error(), "use of closed network connection"))
err := <-errCh
is.Equal(err.Error(), "gemini: Server closed")
}
type (
fastFakeConn struct{}
)
func (*fastFakeConn) Close() error { return nil }
func (*fastFakeConn) Read(b []byte) (int, error) { return copy(b, "gemini://127.0.0.1/\r\n"), nil }
func (*fastFakeConn) Write(b []byte) (n int, err error) { return len(b), nil }
func (*fastFakeConn) RemoteAddr() net.Addr { return &FakeAddr{} }
func (*fastFakeConn) LocalAddr() net.Addr { return &FakeAddr{} }
func (*fastFakeConn) SetDeadline(t time.Time) error { return nil }
func (*fastFakeConn) SetReadDeadline(t time.Time) error { return nil }
func (*fastFakeConn) SetWriteDeadline(t time.Time) error { return nil }
func (*fastFakeConn) ConnectionState() tls.ConnectionState { return tls.ConnectionState{} }
func BenchmarkGig(b *testing.B) {
var (
ok = []byte("ok")
g = New()
conn fastFakeConn
ctx = g.ctxpool.New()
buf = g.bufpool.New()
)
// pre-alloc 1 context and buffer to avoid their allocation during benchmarking
g.ctxpool.New = func() interface{} { return ctx }
g.bufpool.New = func() interface{} { return buf }
g.HidePort = true
g.HideBanner = true
g.Handle("/", func(c Context) error {
return c.GeminiBlob(ok)
})
go func() {
_ = g.Run("127.0.0.1:1965", "_fixture/certs/cert.pem", "_fixture/certs/key.pem")
}()
time.Sleep(200 * time.Millisecond)
defer g.Close()
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
g.handleRequest(&conn)
}
}
Source