v2 / vlib / veb / tests / middleware_test.v
365 lines · 294 sloc · 10.83 KB · 344b9afcfe67902dd9660bd3b077f18464d1d114
Raw
1// vtest build: !windows // fasthttp.Server.run is not implemented on windows yet
2import veb
3import net.http
4import os
5import time
6import compress.gzip
7import compress.zstd
8
9const port = 13001
10
11const localserver = 'http://127.0.0.1:${port}'
12
13const exit_after = time.second * 10
14
15pub struct Context {
16 veb.Context
17pub mut:
18 counter int
19 bound_middleware_tag string
20}
21
22@[heap]
23pub struct App {
24 veb.Middleware[Context]
25mut:
26 started chan bool
27 bound_middleware_hits int
28 bound_middleware_value string
29}
30
31pub fn (mut app App) before_accept_loop() {
32 app.started <- true
33}
34
35pub fn (app &App) index(mut ctx Context) veb.Result {
36 return ctx.text('from index, ${ctx.counter}')
37}
38
39@['/bar/bar']
40pub fn (app &App) bar(mut ctx Context) veb.Result {
41 return ctx.text('from bar, ${ctx.counter}')
42}
43
44pub fn (app &App) unreachable(mut ctx Context) veb.Result {
45 return ctx.text('should never be reachable!')
46}
47
48@['/nested/route/method']
49pub fn (app &App) nested(mut ctx Context) veb.Result {
50 return ctx.text('from nested, ${ctx.counter}')
51}
52
53pub fn (app &App) after(mut ctx Context) veb.Result {
54 return ctx.text('from after, ${ctx.counter}')
55}
56
57@['/admin/auth'; get]
58pub fn (app &App) admin_auth_get(mut ctx Context) veb.Result {
59 return ctx.text('protected get route')
60}
61
62@['/admin/auth'; post]
63pub fn (app &App) admin_auth_post(mut ctx Context) veb.Result {
64 return ctx.text('public post route')
65}
66
67@['/gzip']
68pub fn (app &App) gzip_test(mut ctx Context) veb.Result {
69 return ctx.text('gzip response, ${ctx.counter}')
70}
71
72@['/decode_gzip'; post]
73pub fn (app &App) decode_gzip_test(mut ctx Context) veb.Result {
74 return ctx.text('received: ${ctx.req.data}')
75}
76
77@['/zstd']
78pub fn (app &App) zstd_test(mut ctx Context) veb.Result {
79 return ctx.text('zstd response, ${ctx.counter}')
80}
81
82@['/decode_zstd'; post]
83pub fn (app &App) decode_zstd_test(mut ctx Context) veb.Result {
84 return ctx.text('received: ${ctx.req.data}')
85}
86
87@['/content']
88pub fn (app &App) content_test(mut ctx Context) veb.Result {
89 return ctx.text('content response, ${ctx.counter}')
90}
91
92// Test route for double-send regression test
93@['/double_after']
94pub fn (app &App) double_after(mut ctx Context) veb.Result {
95 return ctx.text('handler response')
96}
97
98// bound verifies that route middleware can read app state before the handler runs.
99@['/bound']
100pub fn (app &App) bound(mut ctx Context) veb.Result {
101 return ctx.text('${app.bound_middleware_value}, ${app.bound_middleware_hits}, ${ctx.bound_middleware_tag}, ${ctx.counter}')
102}
103
104pub fn (app &App) app_middleware(mut ctx Context) bool {
105 ctx.counter++
106 return true
107}
108
109pub fn (app &App) before_request(mut ctx Context) bool {
110 ctx.res.header.add_custom('X-BEFORE-REQUEST-MIDDLEWARE', 'true') or { panic(err) }
111 return true
112}
113
114// route_bound_middleware reads and mutates the bound app to exercise stateful middleware.
115pub fn (mut app App) route_bound_middleware(mut ctx Context) bool {
116 app.bound_middleware_hits++
117 ctx.bound_middleware_tag = app.bound_middleware_value
118 ctx.counter += 10
119 return true
120}
121
122fn middleware_handler(mut ctx Context) bool {
123 ctx.counter++
124 return true
125}
126
127fn middleware_unreachable(mut ctx Context) bool {
128 ctx.text('unreachable, ${ctx.counter}')
129 return false
130}
131
132fn after_middleware(mut ctx Context) bool {
133 ctx.counter++
134 ctx.res.header.add_custom('X-AFTER', ctx.counter.str()) or { panic('bad') }
135 return true
136}
137
138fn auth_required_middleware(mut ctx Context) bool {
139 ctx.res.set_status(.unauthorized)
140 ctx.text('auth required')
141 return false
142}
143
144// Global after-middleware that sends a response (for double-send regression test)
145// Only sends for /double_after route to avoid breaking other tests
146fn global_after_sends_response(mut ctx Context) bool {
147 if ctx.req.url.starts_with('/double_after') {
148 ctx.text('global after response')
149 }
150 return true
151}
152
153// Route after-middleware that also tries to send (should be skipped if global already sent)
154fn route_after_sends_response(mut ctx Context) bool {
155 // Add header to prove this middleware ran (for regression test)
156 ctx.res.header.add_custom('X-ROUTE-AFTER-RAN', 'true') or {}
157 ctx.text('route after response - should not appear')
158 return true
159}
160
161fn testsuite_begin() {
162 os.chdir(os.dir(@FILE))!
163
164 mut app := &App{
165 bound_middleware_value: 'from app middleware'
166 }
167 // even though `route_use` is called first, global middleware is still executed first
168 app.Middleware.route_use('/unreachable', handler: middleware_unreachable)
169
170 // global middleware
171 app.Middleware.use(handler: middleware_handler)
172 app.Middleware.use(handler: app.app_middleware)
173 app.Middleware.use(handler: app.before_request)
174
175 // should match only one slash
176 app.Middleware.route_use('/bar/:foo', handler: middleware_handler)
177 // should match multiple slashes
178 app.Middleware.route_use('/nested/:path...', handler: middleware_handler)
179 app.Middleware.route_use('/bound', handler: app.route_bound_middleware)
180
181 app.Middleware.route_use('/after', handler: after_middleware, after: true)
182 app.Middleware.route_use('/admin/auth',
183 handler: auth_required_middleware
184 methods: [
185 .get,
186 ]
187 )
188
189 // Gzip middleware tests
190 app.Middleware.use(veb.decode_gzip[Context]())
191 app.Middleware.route_use('/gzip', veb.encode_gzip[Context]())
192
193 // Zstd middleware tests
194 app.Middleware.use(veb.decode_zstd[Context]())
195 app.Middleware.route_use('/zstd', veb.encode_zstd[Context]())
196
197 // Auto content encoding middleware (zstd/gzip based on Accept-Encoding)
198 app.Middleware.route_use('/content', veb.encode_auto[Context]())
199
200 // Regression test: global after-middleware sends response, route after-middleware should be skipped
201 app.Middleware.use(handler: global_after_sends_response, after: true)
202 app.Middleware.route_use('/double_after', handler: route_after_sends_response, after: true)
203
204 spawn veb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip)
205 // app startup time
206 _ := <-app.started
207
208 spawn fn () {
209 time.sleep(exit_after)
210 assert true == false, 'timeout reached!'
211 exit(1)
212 }()
213}
214
215fn test_index() {
216 x := http.get(localserver)!
217 assert x.body == 'from index, 2'
218}
219
220fn test_bound_before_request_named_middleware_does_not_timeout() {
221 x := http.fetch(http.FetchConfig{
222 url: localserver
223 read_timeout: 1500 * time.millisecond
224 write_timeout: 1500 * time.millisecond
225 })!
226 assert x.body == 'from index, 2'
227 assert x.header.get_custom('X-BEFORE-REQUEST-MIDDLEWARE')! == 'true'
228}
229
230fn test_unreachable_order() {
231 x := http.get('${localserver}/unreachable')!
232 assert x.body == 'unreachable, 2'
233}
234
235fn test_dynamic_route() {
236 x := http.get('${localserver}/bar/bar')!
237 assert x.body == 'from bar, 3'
238}
239
240fn test_nested() {
241 x := http.get('${localserver}/nested/route/method')!
242 assert x.body == 'from nested, 3'
243}
244
245fn test_route_middleware_bound_method_can_access_app_state() {
246 x := http.get('${localserver}/bound')!
247 assert x.body == 'from app middleware, 1, from app middleware, 12'
248}
249
250fn test_route_middleware_method_filter_get() {
251 x := http.get('${localserver}/admin/auth')!
252 assert x.status() == .unauthorized
253 assert x.body == 'auth required'
254}
255
256fn test_route_middleware_method_filter_post() {
257 x := http.fetch(http.FetchConfig{
258 url: '${localserver}/admin/auth'
259 method: .post
260 })!
261 assert x.status() == .ok
262 assert x.body == 'public post route'
263}
264
265fn test_after_middleware() {
266 x := http.get('${localserver}/after')!
267 assert x.body == 'from after, 2'
268
269 custom_header := x.header.get_custom('X-AFTER') or { panic('should be set!') }
270 assert custom_header == '3'
271}
272
273// Verifies that encode_gzip compresses responses
274fn test_encode_gzip_middleware() {
275 x := http.get('${localserver}/gzip')!
276
277 encoding := x.header.get(.content_encoding) or { '' }
278 assert encoding == 'gzip', 'Expected gzip encoding, got: ${encoding}'
279
280 // HTTP client auto-decompresses gzip content
281 assert x.body == 'gzip response, 2'
282}
283
284// Verifies that decode_gzip middleware decompresses request bodies
285fn test_decode_gzip_middleware() {
286 original_text := 'Hello from gzip compressed body!'
287 compressed := gzip.compress(original_text.bytes())!
288
289 mut req := http.new_request(.post, '${localserver}/decode_gzip', compressed.bytestr())
290 req.header.add(.content_encoding, 'gzip')
291 x := req.do()!
292
293 assert x.body == 'received: ${original_text}'
294}
295
296// Verifies that encode_zstd compresses responses
297fn test_encode_zstd_middleware() {
298 x := http.get('${localserver}/zstd')!
299
300 encoding := x.header.get(.content_encoding) or { '' }
301 assert encoding == 'zstd', 'Expected zstd encoding, got: ${encoding}'
302
303 decompressed := zstd.decompress(x.body.bytes())!
304 assert decompressed.bytestr() == 'zstd response, 2'
305}
306
307// Verifies that decode_zstd middleware decompresses request bodies
308fn test_decode_zstd_middleware() {
309 original_text := 'Hello from zstd compressed body!'
310 compressed := zstd.compress(original_text.bytes())!
311
312 mut req := http.new_request(.post, '${localserver}/decode_zstd', compressed.bytestr())
313 req.header.add(.content_encoding, 'zstd')
314 x := req.do()!
315
316 assert x.body == 'received: ${original_text}'
317}
318
319// Verifies that encode_auto uses zstd when client supports it
320fn test_encode_auto_zstd() {
321 mut req := http.new_request(.get, '${localserver}/content', '')
322 req.header.add(.accept_encoding, 'gzip, zstd, br')
323 x := req.do()!
324
325 encoding := x.header.get(.content_encoding) or { '' }
326 assert encoding == 'zstd', 'Expected zstd encoding when zstd is in Accept-Encoding, got: ${encoding}'
327
328 decompressed := zstd.decompress(x.body.bytes())!
329 assert decompressed.bytestr() == 'content response, 2'
330}
331
332// Verifies that encode_auto falls back to gzip when client doesn't support zstd
333fn test_encode_auto_gzip_fallback() {
334 mut req := http.new_request(.get, '${localserver}/content', '')
335 req.header.add(.accept_encoding, 'gzip, br')
336 x := req.do()!
337
338 encoding := x.header.get(.content_encoding) or { '' }
339 assert encoding == 'gzip', 'Expected gzip encoding when zstd is not in Accept-Encoding, got: ${encoding}'
340
341 // HTTP client auto-decompresses gzip content
342 assert x.body == 'content response, 2'
343}
344
345// Verifies that encode_auto sends uncompressed when no encoding is supported
346fn test_encode_auto_no_compression() {
347 mut req := http.new_request(.get, '${localserver}/content', '')
348 req.header.add(.accept_encoding, 'br')
349 x := req.do()!
350
351 encoding := x.header.get(.content_encoding) or { '' }
352 assert encoding == '', 'Expected no encoding when neither gzip nor zstd is in Accept-Encoding, got: ${encoding}'
353 assert x.body == 'content response, 2'
354}
355
356// Regression test: when global after-middleware sends response, route after-middleware should be skipped
357// This tests the fix for "a response cannot be sent twice over one connection" error
358fn test_double_after_middleware_no_error() {
359 x := http.get('${localserver}/double_after')!
360 // Global after-middleware response should be received
361 assert x.body == 'global after response'
362 // Route after-middleware should NOT have run (header should be absent)
363 route_after_ran := x.header.get_custom('X-ROUTE-AFTER-RAN') or { '' }
364 assert route_after_ran == '', 'Route after-middleware should be skipped when global already sent response'
365}
366