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 }