github.com/abemedia/go-don@v0.2.2-0.20240329015135-be88e32bb73b/handler_test.go (about)

     1  package don_test
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"net/http"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/abemedia/go-don"
    11  	_ "github.com/abemedia/go-don/encoding/json"
    12  	_ "github.com/abemedia/go-don/encoding/text"
    13  	"github.com/abemedia/go-don/pkg/httptest"
    14  	"github.com/abemedia/httprouter"
    15  	"github.com/google/go-cmp/cmp"
    16  	"github.com/valyala/fasthttp"
    17  )
    18  
    19  func TestHandlerRequest(t *testing.T) {
    20  	type request struct {
    21  		Path   string `path:"path"`
    22  		Header string `header:"Header"`
    23  		Query  string `query:"query"`
    24  		JSON   string `json:"json"`
    25  	}
    26  
    27  	var got request
    28  
    29  	want := request{
    30  		Path:   "path",
    31  		Header: "header",
    32  		Query:  "query",
    33  		JSON:   "json",
    34  	}
    35  
    36  	api := don.New(&don.Config{DefaultEncoding: "application/json"})
    37  
    38  	api.Post("/:path", don.H(func(ctx context.Context, req request) (any, error) {
    39  		got = req
    40  		return nil, nil
    41  	}))
    42  
    43  	api.RequestHandler()(httptest.NewRequest(
    44  		fasthttp.MethodPost,
    45  		"/path?query=query",
    46  		`{"json":"json"}`,
    47  		map[string]string{"header": "header"},
    48  	))
    49  
    50  	if diff := cmp.Diff(want, got); diff != "" {
    51  		t.Error(diff)
    52  	}
    53  }
    54  
    55  func TestHandlerResponse(t *testing.T) {
    56  	type request struct {
    57  		url    string
    58  		body   string
    59  		header map[string]string
    60  	}
    61  
    62  	type response struct {
    63  		Code   int
    64  		Body   string
    65  		Header map[string]string
    66  	}
    67  
    68  	tests := []struct {
    69  		message string
    70  		want    response
    71  		config  *don.Config
    72  		route   string
    73  		handler httprouter.Handle
    74  		request request
    75  	}{
    76  		{
    77  			message: "should return no content",
    78  			want: response{
    79  				Code: fasthttp.StatusNoContent,
    80  				Body: "",
    81  				Header: map[string]string{
    82  					"Content-Type": "text/plain; charset=utf-8",
    83  				},
    84  			},
    85  			handler: don.H(func(ctx context.Context, req any) (any, error) {
    86  				return nil, nil
    87  			}),
    88  		},
    89  		{
    90  			message: "should return null",
    91  			want: response{
    92  				Code: fasthttp.StatusOK,
    93  				Body: "null",
    94  				Header: map[string]string{
    95  					"Content-Type": "application/json; charset=utf-8",
    96  				},
    97  			},
    98  			config: &don.Config{DefaultEncoding: "application/json", DisableNoContent: true},
    99  			handler: don.H(func(ctx context.Context, req any) (any, error) {
   100  				return nil, nil
   101  			}),
   102  		},
   103  		{
   104  			message: "should set response headers",
   105  			want: response{
   106  				Code: fasthttp.StatusOK,
   107  				Header: map[string]string{
   108  					"Content-Type": "text/plain; charset=utf-8",
   109  					"Foo":          "bar",
   110  				},
   111  			},
   112  			handler: don.H(func(ctx context.Context, req any) (any, error) {
   113  				return &headerer{}, nil
   114  			}),
   115  		},
   116  		{
   117  			message: "should return error on unprocessable request",
   118  			want: response{
   119  				Code:   fasthttp.StatusUnsupportedMediaType,
   120  				Body:   "Unsupported Media Type",
   121  				Header: map[string]string{"Content-Type": "text/plain; charset=utf-8"},
   122  			},
   123  			handler: don.H(func(ctx context.Context, req struct{ Hello string }) (any, error) {
   124  				return nil, nil
   125  			}),
   126  			request: request{body: `{"foo":"bar"}`},
   127  		},
   128  		{
   129  			message: "should return error on unacceptable",
   130  			want: response{
   131  				Code:   fasthttp.StatusNotAcceptable,
   132  				Body:   "Not Acceptable",
   133  				Header: map[string]string{"Content-Type": "text/plain; charset=utf-8"},
   134  			},
   135  			handler: don.H(func(ctx context.Context, req any) ([]string, error) {
   136  				return []string{"foo", "bar"}, nil
   137  			}),
   138  			request: request{header: map[string]string{"Accept": "text/plain; charset=utf-8"}},
   139  		},
   140  		{
   141  			message: "should return error on unsupported accept",
   142  			want: response{
   143  				Code:   fasthttp.StatusNotAcceptable,
   144  				Body:   "Not Acceptable",
   145  				Header: map[string]string{"Content-Type": "text/plain; charset=utf-8"},
   146  			},
   147  			handler: don.H(func(ctx context.Context, req any) (any, error) {
   148  				return nil, nil
   149  			}),
   150  			request: request{header: map[string]string{"Accept": "application/msword"}},
   151  		},
   152  		{
   153  			message: "should return error on unsupported content type",
   154  			want: response{
   155  				Code:   fasthttp.StatusUnsupportedMediaType,
   156  				Body:   "Unsupported Media Type",
   157  				Header: map[string]string{"Content-Type": "text/plain; charset=utf-8"},
   158  			},
   159  			handler: don.H(func(ctx context.Context, req any) (any, error) {
   160  				return nil, nil
   161  			}),
   162  			request: request{
   163  				body:   `foo`,
   164  				header: map[string]string{"Content-Type": "application/msword"},
   165  			},
   166  		},
   167  		{
   168  			message: "should return error on invalid query",
   169  			want: response{
   170  				Code:   fasthttp.StatusBadRequest,
   171  				Body:   "strconv.ParseInt: parsing \"foo\": invalid syntax",
   172  				Header: map[string]string{"Content-Type": "text/plain; charset=utf-8"},
   173  			},
   174  			handler: don.H(func(ctx context.Context, req struct {
   175  				Test int `query:"test"`
   176  			},
   177  			) (any, error) {
   178  				return req, nil
   179  			}),
   180  			request: request{url: "/?test=foo"},
   181  		},
   182  		{
   183  			message: "should return error on invalid header",
   184  			want: response{
   185  				Code:   fasthttp.StatusBadRequest,
   186  				Body:   "strconv.ParseInt: parsing \"foo\": invalid syntax",
   187  				Header: map[string]string{"Content-Type": "text/plain; charset=utf-8"},
   188  			},
   189  			handler: don.H(func(ctx context.Context, req struct {
   190  				Test int `header:"Test"`
   191  			},
   192  			) (any, error) {
   193  				return req, nil
   194  			}),
   195  			request: request{header: map[string]string{"Test": "foo"}},
   196  		},
   197  		{
   198  			message: "should return error on invalid path element",
   199  			want: response{
   200  				Code:   fasthttp.StatusNotFound,
   201  				Body:   "Not Found",
   202  				Header: map[string]string{"Content-Type": "text/plain; charset=utf-8"},
   203  			},
   204  			handler: don.H(func(ctx context.Context, req struct {
   205  				Test int `path:"test"`
   206  			},
   207  			) (any, error) {
   208  				return req, nil
   209  			}),
   210  			route:   "/:test",
   211  			request: request{url: "/foo"},
   212  		},
   213  		{
   214  			message: "should return error on invalid body",
   215  			want: response{
   216  				Code:   fasthttp.StatusBadRequest,
   217  				Body:   "strconv.Atoi: parsing \"foo\": invalid syntax",
   218  				Header: map[string]string{"Content-Type": "text/plain; charset=utf-8"},
   219  			},
   220  			handler: don.H(func(ctx context.Context, req int) (any, error) {
   221  				return req, nil
   222  			}),
   223  			request: request{body: "foo"},
   224  		},
   225  		{
   226  			message: "should return internal server error",
   227  			want: response{
   228  				Code:   fasthttp.StatusInternalServerError,
   229  				Body:   "test",
   230  				Header: map[string]string{"Content-Type": "text/plain; charset=utf-8"},
   231  			},
   232  			handler: don.H(func(ctx context.Context, req any) (any, error) {
   233  				return nil, errors.New("test")
   234  			}),
   235  		},
   236  	}
   237  
   238  	for _, test := range tests {
   239  		ctx := httptest.NewRequest(fasthttp.MethodPost, test.request.url, test.request.body, test.request.header)
   240  
   241  		api := don.New(test.config)
   242  		api.Post("/"+strings.TrimPrefix(test.route, "/"), test.handler)
   243  		api.RequestHandler()(ctx)
   244  
   245  		res := response{ctx.Response.StatusCode(), string(ctx.Response.Body()), map[string]string{}}
   246  		ctx.Response.Header.VisitAll(func(key, value []byte) { res.Header[string(key)] = string(value) })
   247  
   248  		if diff := cmp.Diff(test.want, res); diff != "" {
   249  			t.Errorf("%s:\n%s", test.message, diff)
   250  		}
   251  	}
   252  }
   253  
   254  type headerer struct{}
   255  
   256  func (h *headerer) String() string {
   257  	return ""
   258  }
   259  
   260  func (h *headerer) Header() http.Header {
   261  	return http.Header{"Foo": []string{"bar"}}
   262  }
   263  
   264  func BenchmarkHandler(b *testing.B) {
   265  	type request struct {
   266  		Path   string `path:"path"`
   267  		Header string `header:"Header"`
   268  		Query  string `query:"query"`
   269  	}
   270  
   271  	header := map[string]string{"Header": "header", "Accept": "text/plain"}
   272  	ctx := httptest.NewRequest("POST", "/path?query=query", "", header)
   273  	p := httprouter.Params{{Key: "path", Value: "path"}}
   274  
   275  	b.Run("Request", func(b *testing.B) {
   276  		h := don.H(func(ctx context.Context, req request) (any, error) { return nil, nil })
   277  		for i := 0; i < b.N; i++ {
   278  			h(ctx, p)
   279  		}
   280  	})
   281  
   282  	b.Run("RequestPointer", func(b *testing.B) {
   283  		h := don.H(func(ctx context.Context, req *request) (any, error) { return nil, nil })
   284  		for i := 0; i < b.N; i++ {
   285  			h(ctx, p)
   286  		}
   287  	})
   288  }