366 lines
11 KiB
V
366 lines
11 KiB
V
// vtest build: !windows // fasthttp.Server.run is not implemented on windows yet
|
|
import veb
|
|
import net.http
|
|
import os
|
|
import time
|
|
import compress.gzip
|
|
import compress.zstd
|
|
|
|
const port = 13001
|
|
|
|
const localserver = 'http://127.0.0.1:${port}'
|
|
|
|
const exit_after = time.second * 10
|
|
|
|
pub struct Context {
|
|
veb.Context
|
|
pub mut:
|
|
counter int
|
|
bound_middleware_tag string
|
|
}
|
|
|
|
@[heap]
|
|
pub struct App {
|
|
veb.Middleware[Context]
|
|
mut:
|
|
started chan bool
|
|
bound_middleware_hits int
|
|
bound_middleware_value string
|
|
}
|
|
|
|
pub fn (mut app App) before_accept_loop() {
|
|
app.started <- true
|
|
}
|
|
|
|
pub fn (app &App) index(mut ctx Context) veb.Result {
|
|
return ctx.text('from index, ${ctx.counter}')
|
|
}
|
|
|
|
@['/bar/bar']
|
|
pub fn (app &App) bar(mut ctx Context) veb.Result {
|
|
return ctx.text('from bar, ${ctx.counter}')
|
|
}
|
|
|
|
pub fn (app &App) unreachable(mut ctx Context) veb.Result {
|
|
return ctx.text('should never be reachable!')
|
|
}
|
|
|
|
@['/nested/route/method']
|
|
pub fn (app &App) nested(mut ctx Context) veb.Result {
|
|
return ctx.text('from nested, ${ctx.counter}')
|
|
}
|
|
|
|
pub fn (app &App) after(mut ctx Context) veb.Result {
|
|
return ctx.text('from after, ${ctx.counter}')
|
|
}
|
|
|
|
@['/admin/auth'; get]
|
|
pub fn (app &App) admin_auth_get(mut ctx Context) veb.Result {
|
|
return ctx.text('protected get route')
|
|
}
|
|
|
|
@['/admin/auth'; post]
|
|
pub fn (app &App) admin_auth_post(mut ctx Context) veb.Result {
|
|
return ctx.text('public post route')
|
|
}
|
|
|
|
@['/gzip']
|
|
pub fn (app &App) gzip_test(mut ctx Context) veb.Result {
|
|
return ctx.text('gzip response, ${ctx.counter}')
|
|
}
|
|
|
|
@['/decode_gzip'; post]
|
|
pub fn (app &App) decode_gzip_test(mut ctx Context) veb.Result {
|
|
return ctx.text('received: ${ctx.req.data}')
|
|
}
|
|
|
|
@['/zstd']
|
|
pub fn (app &App) zstd_test(mut ctx Context) veb.Result {
|
|
return ctx.text('zstd response, ${ctx.counter}')
|
|
}
|
|
|
|
@['/decode_zstd'; post]
|
|
pub fn (app &App) decode_zstd_test(mut ctx Context) veb.Result {
|
|
return ctx.text('received: ${ctx.req.data}')
|
|
}
|
|
|
|
@['/content']
|
|
pub fn (app &App) content_test(mut ctx Context) veb.Result {
|
|
return ctx.text('content response, ${ctx.counter}')
|
|
}
|
|
|
|
// Test route for double-send regression test
|
|
@['/double_after']
|
|
pub fn (app &App) double_after(mut ctx Context) veb.Result {
|
|
return ctx.text('handler response')
|
|
}
|
|
|
|
// bound verifies that route middleware can read app state before the handler runs.
|
|
@['/bound']
|
|
pub fn (app &App) bound(mut ctx Context) veb.Result {
|
|
return ctx.text('${app.bound_middleware_value}, ${app.bound_middleware_hits}, ${ctx.bound_middleware_tag}, ${ctx.counter}')
|
|
}
|
|
|
|
pub fn (app &App) app_middleware(mut ctx Context) bool {
|
|
ctx.counter++
|
|
return true
|
|
}
|
|
|
|
pub fn (app &App) before_request(mut ctx Context) bool {
|
|
ctx.res.header.add_custom('X-BEFORE-REQUEST-MIDDLEWARE', 'true') or { panic(err) }
|
|
return true
|
|
}
|
|
|
|
// route_bound_middleware reads and mutates the bound app to exercise stateful middleware.
|
|
pub fn (mut app App) route_bound_middleware(mut ctx Context) bool {
|
|
app.bound_middleware_hits++
|
|
ctx.bound_middleware_tag = app.bound_middleware_value
|
|
ctx.counter += 10
|
|
return true
|
|
}
|
|
|
|
fn middleware_handler(mut ctx Context) bool {
|
|
ctx.counter++
|
|
return true
|
|
}
|
|
|
|
fn middleware_unreachable(mut ctx Context) bool {
|
|
ctx.text('unreachable, ${ctx.counter}')
|
|
return false
|
|
}
|
|
|
|
fn after_middleware(mut ctx Context) bool {
|
|
ctx.counter++
|
|
ctx.res.header.add_custom('X-AFTER', ctx.counter.str()) or { panic('bad') }
|
|
return true
|
|
}
|
|
|
|
fn auth_required_middleware(mut ctx Context) bool {
|
|
ctx.res.set_status(.unauthorized)
|
|
ctx.text('auth required')
|
|
return false
|
|
}
|
|
|
|
// Global after-middleware that sends a response (for double-send regression test)
|
|
// Only sends for /double_after route to avoid breaking other tests
|
|
fn global_after_sends_response(mut ctx Context) bool {
|
|
if ctx.req.url.starts_with('/double_after') {
|
|
ctx.text('global after response')
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Route after-middleware that also tries to send (should be skipped if global already sent)
|
|
fn route_after_sends_response(mut ctx Context) bool {
|
|
// Add header to prove this middleware ran (for regression test)
|
|
ctx.res.header.add_custom('X-ROUTE-AFTER-RAN', 'true') or {}
|
|
ctx.text('route after response - should not appear')
|
|
return true
|
|
}
|
|
|
|
fn testsuite_begin() {
|
|
os.chdir(os.dir(@FILE))!
|
|
|
|
mut app := &App{
|
|
bound_middleware_value: 'from app middleware'
|
|
}
|
|
// even though `route_use` is called first, global middleware is still executed first
|
|
app.Middleware.route_use('/unreachable', handler: middleware_unreachable)
|
|
|
|
// global middleware
|
|
app.Middleware.use(handler: middleware_handler)
|
|
app.Middleware.use(handler: app.app_middleware)
|
|
app.Middleware.use(handler: app.before_request)
|
|
|
|
// should match only one slash
|
|
app.Middleware.route_use('/bar/:foo', handler: middleware_handler)
|
|
// should match multiple slashes
|
|
app.Middleware.route_use('/nested/:path...', handler: middleware_handler)
|
|
app.Middleware.route_use('/bound', handler: app.route_bound_middleware)
|
|
|
|
app.Middleware.route_use('/after', handler: after_middleware, after: true)
|
|
app.Middleware.route_use('/admin/auth',
|
|
handler: auth_required_middleware
|
|
methods: [
|
|
.get,
|
|
]
|
|
)
|
|
|
|
// Gzip middleware tests
|
|
app.Middleware.use(veb.decode_gzip[Context]())
|
|
app.Middleware.route_use('/gzip', veb.encode_gzip[Context]())
|
|
|
|
// Zstd middleware tests
|
|
app.Middleware.use(veb.decode_zstd[Context]())
|
|
app.Middleware.route_use('/zstd', veb.encode_zstd[Context]())
|
|
|
|
// Auto content encoding middleware (zstd/gzip based on Accept-Encoding)
|
|
app.Middleware.route_use('/content', veb.encode_auto[Context]())
|
|
|
|
// Regression test: global after-middleware sends response, route after-middleware should be skipped
|
|
app.Middleware.use(handler: global_after_sends_response, after: true)
|
|
app.Middleware.route_use('/double_after', handler: route_after_sends_response, after: true)
|
|
|
|
spawn veb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip)
|
|
// app startup time
|
|
_ := <-app.started
|
|
|
|
spawn fn () {
|
|
time.sleep(exit_after)
|
|
assert true == false, 'timeout reached!'
|
|
exit(1)
|
|
}()
|
|
}
|
|
|
|
fn test_index() {
|
|
x := http.get(localserver)!
|
|
assert x.body == 'from index, 2'
|
|
}
|
|
|
|
fn test_bound_before_request_named_middleware_does_not_timeout() {
|
|
x := http.fetch(http.FetchConfig{
|
|
url: localserver
|
|
read_timeout: 1500 * time.millisecond
|
|
write_timeout: 1500 * time.millisecond
|
|
})!
|
|
assert x.body == 'from index, 2'
|
|
assert x.header.get_custom('X-BEFORE-REQUEST-MIDDLEWARE')? == 'true'
|
|
}
|
|
|
|
fn test_unreachable_order() {
|
|
x := http.get('${localserver}/unreachable')!
|
|
assert x.body == 'unreachable, 2'
|
|
}
|
|
|
|
fn test_dynamic_route() {
|
|
x := http.get('${localserver}/bar/bar')!
|
|
assert x.body == 'from bar, 3'
|
|
}
|
|
|
|
fn test_nested() {
|
|
x := http.get('${localserver}/nested/route/method')!
|
|
assert x.body == 'from nested, 3'
|
|
}
|
|
|
|
fn test_route_middleware_bound_method_can_access_app_state() {
|
|
x := http.get('${localserver}/bound')!
|
|
assert x.body == 'from app middleware, 1, from app middleware, 12'
|
|
}
|
|
|
|
fn test_route_middleware_method_filter_get() {
|
|
x := http.get('${localserver}/admin/auth')!
|
|
assert x.status() == .unauthorized
|
|
assert x.body == 'auth required'
|
|
}
|
|
|
|
fn test_route_middleware_method_filter_post() {
|
|
x := http.fetch(http.FetchConfig{
|
|
url: '${localserver}/admin/auth'
|
|
method: .post
|
|
})!
|
|
assert x.status() == .ok
|
|
assert x.body == 'public post route'
|
|
}
|
|
|
|
fn test_after_middleware() {
|
|
x := http.get('${localserver}/after')!
|
|
assert x.body == 'from after, 2'
|
|
|
|
custom_header := x.header.get_custom('X-AFTER') or { panic('should be set!') }
|
|
assert custom_header == '3'
|
|
}
|
|
|
|
// Verifies that encode_gzip compresses responses
|
|
fn test_encode_gzip_middleware() {
|
|
x := http.get('${localserver}/gzip')!
|
|
|
|
encoding := x.header.get(.content_encoding) or { '' }
|
|
assert encoding == 'gzip', 'Expected gzip encoding, got: ${encoding}'
|
|
|
|
// HTTP client auto-decompresses gzip content
|
|
assert x.body == 'gzip response, 2'
|
|
}
|
|
|
|
// Verifies that decode_gzip middleware decompresses request bodies
|
|
fn test_decode_gzip_middleware() {
|
|
original_text := 'Hello from gzip compressed body!'
|
|
compressed := gzip.compress(original_text.bytes())!
|
|
|
|
mut req := http.new_request(.post, '${localserver}/decode_gzip', compressed.bytestr())
|
|
req.header.add(.content_encoding, 'gzip')
|
|
x := req.do()!
|
|
|
|
assert x.body == 'received: ${original_text}'
|
|
}
|
|
|
|
// Verifies that encode_zstd compresses responses
|
|
fn test_encode_zstd_middleware() {
|
|
x := http.get('${localserver}/zstd')!
|
|
|
|
encoding := x.header.get(.content_encoding) or { '' }
|
|
assert encoding == 'zstd', 'Expected zstd encoding, got: ${encoding}'
|
|
|
|
decompressed := zstd.decompress(x.body.bytes())!
|
|
assert decompressed.bytestr() == 'zstd response, 2'
|
|
}
|
|
|
|
// Verifies that decode_zstd middleware decompresses request bodies
|
|
fn test_decode_zstd_middleware() {
|
|
original_text := 'Hello from zstd compressed body!'
|
|
compressed := zstd.compress(original_text.bytes())!
|
|
|
|
mut req := http.new_request(.post, '${localserver}/decode_zstd', compressed.bytestr())
|
|
req.header.add(.content_encoding, 'zstd')
|
|
x := req.do()!
|
|
|
|
assert x.body == 'received: ${original_text}'
|
|
}
|
|
|
|
// Verifies that encode_auto uses zstd when client supports it
|
|
fn test_encode_auto_zstd() {
|
|
mut req := http.new_request(.get, '${localserver}/content', '')
|
|
req.header.add(.accept_encoding, 'gzip, zstd, br')
|
|
x := req.do()!
|
|
|
|
encoding := x.header.get(.content_encoding) or { '' }
|
|
assert encoding == 'zstd', 'Expected zstd encoding when zstd is in Accept-Encoding, got: ${encoding}'
|
|
|
|
decompressed := zstd.decompress(x.body.bytes())!
|
|
assert decompressed.bytestr() == 'content response, 2'
|
|
}
|
|
|
|
// Verifies that encode_auto falls back to gzip when client doesn't support zstd
|
|
fn test_encode_auto_gzip_fallback() {
|
|
mut req := http.new_request(.get, '${localserver}/content', '')
|
|
req.header.add(.accept_encoding, 'gzip, br')
|
|
x := req.do()!
|
|
|
|
encoding := x.header.get(.content_encoding) or { '' }
|
|
assert encoding == 'gzip', 'Expected gzip encoding when zstd is not in Accept-Encoding, got: ${encoding}'
|
|
|
|
// HTTP client auto-decompresses gzip content
|
|
assert x.body == 'content response, 2'
|
|
}
|
|
|
|
// Verifies that encode_auto sends uncompressed when no encoding is supported
|
|
fn test_encode_auto_no_compression() {
|
|
mut req := http.new_request(.get, '${localserver}/content', '')
|
|
req.header.add(.accept_encoding, 'br')
|
|
x := req.do()!
|
|
|
|
encoding := x.header.get(.content_encoding) or { '' }
|
|
assert encoding == '', 'Expected no encoding when neither gzip nor zstd is in Accept-Encoding, got: ${encoding}'
|
|
assert x.body == 'content response, 2'
|
|
}
|
|
|
|
// Regression test: when global after-middleware sends response, route after-middleware should be skipped
|
|
// This tests the fix for "a response cannot be sent twice over one connection" error
|
|
fn test_double_after_middleware_no_error() {
|
|
x := http.get('${localserver}/double_after')!
|
|
// Global after-middleware response should be received
|
|
assert x.body == 'global after response'
|
|
// Route after-middleware should NOT have run (header should be absent)
|
|
route_after_ran := x.header.get_custom('X-ROUTE-AFTER-RAN') or { '' }
|
|
assert route_after_ran == '', 'Route after-middleware should be skipped when global already sent response'
|
|
}
|