-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrequest_middleware_test.go
More file actions
329 lines (275 loc) · 10.1 KB
/
Copy pathrequest_middleware_test.go
File metadata and controls
329 lines (275 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package httpserver
import (
"net/http"
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Tests for [MiddlewareParser]: a typed middleware that parses the request
// before calling next. All tests go through the real [Router.Handle] / ServeMux
// path via [newTestRouter] and [doRouterRequest].
// Successful parser middleware binds request data and calls downstream exactly
// once.
func TestMiddlewareParser_BindsData_CallsDownstreamOnce(t *testing.T) {
type MWReq struct {
UserID string `header:"X-User-Id"`
}
var capturedUserID string
var downstreamCalls int32
mw := MiddlewareParser(func(ctx *Context, req MWReq, next func()) {
capturedUserID = req.UserID
next()
})
router := newTestRouter().Group(mw)
router.Handle("GET /", func(ctx *Context) {
atomic.AddInt32(&downstreamCalls, 1)
ctx.NewResponse(http.StatusOK)
})
rec := doRouterRequest(router, http.MethodGet, "/", withHeader("X-User-Id", "user123"))
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "user123", capturedUserID, "middleware should capture bound header")
assert.Equal(t, int32(1), atomic.LoadInt32(&downstreamCalls), "downstream called exactly once")
}
// Defaults are applied before the middleware handler runs.
func TestMiddlewareParser_DefaultsAppliedBeforeHandler(t *testing.T) {
type MWReq struct {
Role string `default:"guest"`
}
var capturedRole string
mw := MiddlewareParser(func(ctx *Context, req MWReq, next func()) {
capturedRole = req.Role
next()
})
router := newTestRouter().Group(mw)
router.Handle("GET /", func(ctx *Context) {
ctx.NewResponse(http.StatusOK)
})
rec := doRouterRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "guest", capturedRole, "default should be applied before handler")
}
// Validation failure sets 400 and does not call downstream.
func TestMiddlewareParser_ValidationFailure_400_NoDownstream(t *testing.T) {
type MWReq struct {
Name string `query:"name" validate:"required"`
}
var mwHandlerCalled, downstreamCalled bool
mw := MiddlewareParser(func(ctx *Context, req MWReq, next func()) {
mwHandlerCalled = true
next()
})
router := newTestRouter().Group(mw)
router.Handle("GET /", func(ctx *Context) {
downstreamCalled = true
ctx.NewResponse(http.StatusOK)
})
rec := doRouterRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusBadRequest, rec.Code)
assert.False(t, mwHandlerCalled, "middleware handler should not run on validation failure")
assert.False(t, downstreamCalled, "downstream should not run on validation failure")
}
// Parse/bind failure sets the expected status and does not call downstream.
func TestMiddlewareParser_BindFailure_400_NoDownstream(t *testing.T) {
type MWReq struct {
Count int `header:"X-Count"`
}
var mwHandlerCalled, downstreamCalled bool
mw := MiddlewareParser(func(ctx *Context, req MWReq, next func()) {
mwHandlerCalled = true
next()
})
router := newTestRouter().Group(mw)
router.Handle("GET /", func(ctx *Context) {
downstreamCalled = true
ctx.NewResponse(http.StatusOK)
})
rec := doRouterRequest(router, http.MethodGet, "/", withHeader("X-Count", "not-a-number"))
assert.Equal(t, http.StatusBadRequest, rec.Code)
assert.False(t, mwHandlerCalled, "middleware handler should not run on bind failure")
assert.False(t, downstreamCalled, "downstream should not run on bind failure")
}
// Parsed middleware and a downstream [RequestParser] share the same [*Context]
// and response state.
func TestMiddlewareParser_SharesContextWithDownstreamRequestParser(t *testing.T) {
type MWReq struct {
Token string `header:"X-Token"`
}
type HandlerReq struct {
Name string `query:"name"`
}
var mwCtx, handlerCtx *Context
var statusAfterNext int
mw := MiddlewareParser(func(ctx *Context, req MWReq, next func()) {
mwCtx = ctx
next()
statusAfterNext = ctx.Response().Status()
})
router := newTestRouter().Group(mw)
router.Handle("GET /", RequestParser(func(ctx *Context, req HandlerReq) {
handlerCtx = ctx
ctx.NewResponse(http.StatusOK)
}))
rec := doRouterRequest(router, http.MethodGet, "/?name=alice", withHeader("X-Token", "tok123"))
assert.Equal(t, http.StatusOK, rec.Code)
require.NotNil(t, mwCtx)
require.NotNil(t, handlerCtx)
assert.Same(t, mwCtx, handlerCtx, "middleware and downstream must share the same *Context")
assert.Equal(t, http.StatusOK, statusAfterNext, "middleware should see downstream's status via shared Context")
}
// A middleware can inspect the downstream response after next and then
// modify/replace it.
func TestMiddlewareParser_InspectAndReplaceDownstreamResponse(t *testing.T) {
type MWReq struct{}
mw := MiddlewareParser(func(ctx *Context, req MWReq, next func()) {
next()
if ctx.Response().Status() == http.StatusOK {
ctx.NewResponse(http.StatusTeapot).StringBody("replaced")
}
})
router := newTestRouter().Group(mw)
router.Handle("GET /", func(ctx *Context) {
ctx.NewResponse(http.StatusOK).StringBody("original")
})
rec := doRouterRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusTeapot, rec.Code)
assert.Equal(t, "replaced", rec.Body.String())
}
// A middleware can configure a response and short-circuit without calling
// next.
func TestMiddlewareParser_ShortCircuitWithoutNext(t *testing.T) {
type MWReq struct{}
var downstreamCalled bool
mw := MiddlewareParser(func(ctx *Context, req MWReq, next func()) {
ctx.NewResponse(http.StatusTeapot).StringBody("short-circuit")
})
router := newTestRouter().Group(mw)
router.Handle("GET /", func(ctx *Context) {
downstreamCalled = true
ctx.NewResponse(http.StatusOK)
})
rec := doRouterRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusTeapot, rec.Code)
assert.Equal(t, "short-circuit", rec.Body.String())
assert.False(t, downstreamCalled, "downstream must not run when middleware short-circuits")
}
// A chain that finishes without any response still becomes 500 via
// [Router.Handle].
func TestMiddlewareParser_ChainWithoutResponse_Becomes500(t *testing.T) {
type MWReq struct{}
mw := MiddlewareParser(func(ctx *Context, req MWReq, next func()) {
next()
})
router := newTestRouter().Group(mw)
router.Handle("GET /", func(ctx *Context) {
// no response configured
})
rec := doRouterRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusInternalServerError, rec.Code)
}
// Lock in the documented body behavior.
//
// header/query-only parser middleware followed by a body parser succeeds: the
// middleware does not touch the body, so the downstream [RequestParser] can
// still read it.
func TestMiddlewareParser_HeaderMiddleware_ThenBodyParser_Succeeds(t *testing.T) {
type MWReq struct {
Token string `header:"X-Token"`
}
type HandlerReq struct {
Name string `json:"name"`
}
var capturedToken, capturedName string
mw := MiddlewareParser(func(ctx *Context, req MWReq, next func()) {
capturedToken = req.Token
next()
})
router := newTestRouter().Group(mw)
router.Handle("POST /", RequestParser(func(ctx *Context, req HandlerReq) {
capturedName = req.Name
ctx.NewResponse(http.StatusOK)
}))
rec := doRouterRequest(router, http.MethodPost, "/",
withHeader("X-Token", "tok123"),
withJSONBody(map[string]any{"name": "alice"}),
)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "tok123", capturedToken, "middleware should capture token from header")
assert.Equal(t, "alice", capturedName, "handler should capture name from JSON body")
}
// Two parser layers that both consume the body are not silently rewound or
// replayed: the second consumer hits EOF and fails.
func TestMiddlewareParser_TwoBodyConsumers_SecondFails(t *testing.T) {
type MWReq struct {
Token string `json:"token"`
}
type HandlerReq struct {
Name string `json:"name"`
}
var mwHandlerCalled, downstreamHandlerCalled bool
mw := MiddlewareParser(func(ctx *Context, req MWReq, next func()) {
mwHandlerCalled = true
next()
})
router := newTestRouter().Group(mw)
router.Handle("POST /", RequestParser(func(ctx *Context, req HandlerReq) {
downstreamHandlerCalled = true
ctx.NewResponse(http.StatusOK)
}))
rec := doRouterRequest(router, http.MethodPost, "/",
withJSONBody(map[string]any{"token": "tok", "name": "alice"}),
)
// Middleware consumed the body; downstream JSON parser sees EOF → 400.
assert.Equal(t, http.StatusBadRequest, rec.Code,
"second body consumer should fail because body is already consumed")
assert.True(t, mwHandlerCalled, "middleware handler should have run (first consumer)")
assert.False(t, downstreamHandlerCalled, "downstream handler should not run (body parse failed)")
}
// Add one nested [Router.Group]([MiddlewareParser]) case so group composition
// and typed middleware are tested together.
func TestMiddlewareParser_NestedGroupComposition(t *testing.T) {
type OuterMWReq struct {
Token string `header:"X-Token"`
}
type InnerMWReq struct {
APIKey string `header:"X-Api-Key"`
}
type HandlerReq struct {
Name string `query:"name"`
}
var trace []string
outerMW := MiddlewareParser(func(ctx *Context, req OuterMWReq, next func()) {
trace = append(trace, "outer-before:"+req.Token)
next()
trace = append(trace, "outer-after")
})
innerMW := MiddlewareParser(func(ctx *Context, req InnerMWReq, next func()) {
trace = append(trace, "inner-before:"+req.APIKey)
next()
trace = append(trace, "inner-after")
})
router := newTestRouter()
outer := router.Group(outerMW)
inner := outer.Group(innerMW)
inner.Handle("GET /", RequestParser(func(ctx *Context, req HandlerReq) {
trace = append(trace, "handler:"+req.Name)
ctx.NewResponse(http.StatusOK)
}))
rec := doRouterRequest(router, http.MethodGet, "/?name=alice",
withHeader("X-Token", "tok"),
withHeader("X-Api-Key", "key"),
)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, []string{
"outer-before:tok",
"inner-before:key",
"handler:alice",
"inner-after",
"outer-after",
}, trace, "middleware execution order must be outer→inner→handler→inner→outer")
}