Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,365 @@
|
||||
// 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'
|
||||
}
|
||||
Reference in new issue
Block a user