feat: add 7 built-in middleware components inspired by Hono.js
New Middleware: - CORS: Cross-Origin Resource Sharing with full configuration support - Cookie Helper: Cookie management with signed cookie support (HMAC-SHA256) - JWT: JSON Web Token authentication (HS256/384/512) - Bearer Auth: Simple Bearer token authentication - Compression: Gzip and deflate response compression - Rate Limiting: Request rate limiting with customizable stores - Validator: Schema-based request validation Features: - Context store for middleware data sharing (get/set/get_client_ip) - Middleware export module with utility functions - Comprehensive property-based tests for all middleware - Example file demonstrating all middleware usage Files Added: - cors.v, cookie.v, jwt.v, bearer_auth.v, compress.v, rate_limit.v, validator.v - middleware.v (unified exports) - examples/middleware_demo.v - tests/test_*.v (9 new test files) Documentation: - Updated README.md with detailed middleware usage guide - Added Schema Builder API documentation
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
import meiseayoung.hono
|
||||
import net.http
|
||||
import rand
|
||||
import time
|
||||
|
||||
// CORS Middleware 属性测试
|
||||
// Property-Based Testing for CORS functionality
|
||||
|
||||
const test_iterations = 100
|
||||
|
||||
struct CorsTestStats {
|
||||
mut:
|
||||
total_tests int
|
||||
passed_tests int
|
||||
failed_tests int
|
||||
}
|
||||
|
||||
fn (mut stats CorsTestStats) run_property_test(test_name string, test_func fn () bool) {
|
||||
stats.total_tests++
|
||||
print('🔬 ${test_name}... ')
|
||||
|
||||
if test_func() {
|
||||
stats.passed_tests++
|
||||
println('✅')
|
||||
} else {
|
||||
stats.failed_tests++
|
||||
println('❌')
|
||||
}
|
||||
}
|
||||
|
||||
fn (stats CorsTestStats) print_summary() {
|
||||
println('\n=== CORS Middleware 属性测试总结 ===')
|
||||
println('总测试数: ${stats.total_tests}')
|
||||
println('通过: ${stats.passed_tests}')
|
||||
println('失败: ${stats.failed_tests}')
|
||||
|
||||
if stats.failed_tests == 0 {
|
||||
println('🎉 所有属性测试通过!')
|
||||
} else {
|
||||
println('⚠️ 有 ${stats.failed_tests} 个属性测试失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 创建带 Origin 头的测试 Context
|
||||
fn create_cors_context_with_origin(origin string, method http.Method) hono.Context {
|
||||
mut headers := http.new_header()
|
||||
if origin.len > 0 {
|
||||
headers.add_custom('Origin', origin) or {}
|
||||
}
|
||||
|
||||
req := http.Request{
|
||||
method: method
|
||||
url: '/test'
|
||||
header: headers
|
||||
}
|
||||
return hono.Context.new(req, map[string]string{}, map[string]string{}, '')
|
||||
}
|
||||
|
||||
// 创建带 Origin 和 Access-Control-Request-Headers 的预检请求 Context
|
||||
fn create_cors_preflight_context(origin string, request_headers string) hono.Context {
|
||||
mut headers := http.new_header()
|
||||
if origin.len > 0 {
|
||||
headers.add_custom('Origin', origin) or {}
|
||||
}
|
||||
if request_headers.len > 0 {
|
||||
headers.add_custom('Access-Control-Request-Headers', request_headers) or {}
|
||||
}
|
||||
|
||||
req := http.Request{
|
||||
method: .options
|
||||
url: '/test'
|
||||
header: headers
|
||||
}
|
||||
return hono.Context.new(req, map[string]string{}, map[string]string{}, '')
|
||||
}
|
||||
|
||||
// 生成随机的域名
|
||||
fn generate_cors_random_origin() string {
|
||||
protocols := ['http', 'https']
|
||||
domains := ['example', 'test', 'api', 'app', 'web', 'service', 'backend', 'frontend']
|
||||
tlds := ['com', 'org', 'net', 'io', 'dev', 'app']
|
||||
|
||||
protocol := protocols[rand.int_in_range(0, protocols.len) or { 0 }]
|
||||
domain := domains[rand.int_in_range(0, domains.len) or { 0 }]
|
||||
tld := tlds[rand.int_in_range(0, tlds.len) or { 0 }]
|
||||
|
||||
// 有时添加端口
|
||||
port := if rand.int_in_range(0, 3) or { 0 } == 0 {
|
||||
':${rand.int_in_range(3000, 9000) or { 8080 }}'
|
||||
} else {
|
||||
''
|
||||
}
|
||||
|
||||
return '${protocol}://${domain}.${tld}${port}'
|
||||
}
|
||||
|
||||
// 生成随机的 HTTP 方法
|
||||
fn generate_cors_random_method() http.Method {
|
||||
methods := [http.Method.get, http.Method.post, http.Method.put, http.Method.delete, http.Method.patch]
|
||||
return methods[rand.int_in_range(0, methods.len) or { 0 }]
|
||||
}
|
||||
|
||||
// 生成随机的请求头列表
|
||||
fn generate_cors_random_headers() []string {
|
||||
all_headers := ['Content-Type', 'Authorization', 'X-Custom-Header', 'X-Request-ID', 'Accept', 'Cache-Control']
|
||||
count := rand.int_in_range(1, all_headers.len) or { 2 }
|
||||
mut result := []string{}
|
||||
for i in 0 .. count {
|
||||
result << all_headers[i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 模拟 next 函数,返回一个简单的响应
|
||||
fn cors_mock_next(mut c hono.Context) http.Response {
|
||||
return c.text('OK')
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Property 1: CORS Origin Header Consistency
|
||||
// Feature: builtin-middleware, Property 1: CORS Origin Header Consistency
|
||||
// Validates: Requirements 1.1, 1.3, 1.4, 1.5, 1.6
|
||||
//
|
||||
// *For any* request with an Origin header and any CORS configuration, the
|
||||
// Access-Control-Allow-Origin response header SHALL match the configured
|
||||
// origin policy (wildcard "*", specific domain, or callback result).
|
||||
// ============================================================================
|
||||
fn test_property_1_cors_origin_header_consistency() bool {
|
||||
rand.seed([u32(time.now().unix()), u32(12345)])
|
||||
|
||||
// Test 1: Wildcard origin ("*") should allow all origins
|
||||
for _ in 0 .. test_iterations / 4 {
|
||||
origin := generate_cors_random_origin()
|
||||
mut ctx := create_cors_context_with_origin(origin, generate_cors_random_method())
|
||||
|
||||
cors_mw := hono.cors()
|
||||
_ := cors_mw(mut ctx, cors_mock_next)
|
||||
|
||||
allowed_origin := ctx.headers['Access-Control-Allow-Origin'] or { '' }
|
||||
if allowed_origin != '*' {
|
||||
println(' Wildcard test failed: expected "*", got "${allowed_origin}"')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: Specific domain should only allow that domain
|
||||
for _ in 0 .. test_iterations / 4 {
|
||||
allowed_domain := generate_cors_random_origin()
|
||||
request_origin := generate_cors_random_origin()
|
||||
|
||||
mut ctx := create_cors_context_with_origin(request_origin, generate_cors_random_method())
|
||||
|
||||
cors_mw := hono.cors(hono.CorsOptions{
|
||||
origin: allowed_domain
|
||||
})
|
||||
_ := cors_mw(mut ctx, cors_mock_next)
|
||||
|
||||
result_origin := ctx.headers['Access-Control-Allow-Origin'] or { '' }
|
||||
|
||||
if request_origin == allowed_domain {
|
||||
// Should be allowed
|
||||
if result_origin != request_origin {
|
||||
println(' Specific domain test failed: expected "${request_origin}", got "${result_origin}"')
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// Should not be allowed (no header set)
|
||||
if result_origin != '' {
|
||||
println(' Specific domain test failed: expected empty, got "${result_origin}"')
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: Array of domains should allow any domain in the array
|
||||
for _ in 0 .. test_iterations / 4 {
|
||||
allowed_domains := [generate_cors_random_origin(), generate_cors_random_origin(), generate_cors_random_origin()]
|
||||
|
||||
// Test with an allowed domain
|
||||
allowed_idx := rand.int_in_range(0, allowed_domains.len) or { 0 }
|
||||
request_origin := allowed_domains[allowed_idx]
|
||||
|
||||
mut ctx := create_cors_context_with_origin(request_origin, generate_cors_random_method())
|
||||
|
||||
cors_mw := hono.cors(hono.CorsOptions{
|
||||
origin: allowed_domains
|
||||
})
|
||||
_ := cors_mw(mut ctx, cors_mock_next)
|
||||
|
||||
result_origin := ctx.headers['Access-Control-Allow-Origin'] or { '' }
|
||||
if result_origin != request_origin {
|
||||
println(' Array domain test failed: expected "${request_origin}", got "${result_origin}"')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Test 4: Callback function should use callback result
|
||||
for _ in 0 .. test_iterations / 4 {
|
||||
request_origin := generate_cors_random_origin()
|
||||
|
||||
mut ctx := create_cors_context_with_origin(request_origin, generate_cors_random_method())
|
||||
|
||||
// Callback that returns the origin if it starts with "https"
|
||||
cors_mw := hono.cors(hono.CorsOptions{
|
||||
origin: fn (origin string, c hono.Context) string {
|
||||
if origin.starts_with('https') {
|
||||
return origin
|
||||
}
|
||||
return ''
|
||||
}
|
||||
})
|
||||
_ := cors_mw(mut ctx, cors_mock_next)
|
||||
|
||||
result_origin := ctx.headers['Access-Control-Allow-Origin'] or { '' }
|
||||
|
||||
if request_origin.starts_with('https') {
|
||||
if result_origin != request_origin {
|
||||
println(' Callback test failed: expected "${request_origin}", got "${result_origin}"')
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if result_origin != '' {
|
||||
println(' Callback test failed: expected empty for non-https, got "${result_origin}"')
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Property 2: CORS Preflight Response
|
||||
// Feature: builtin-middleware, Property 2: CORS Preflight Response
|
||||
// Validates: Requirements 1.2, 1.7, 1.8, 1.9, 1.10, 1.11
|
||||
//
|
||||
// *For any* OPTIONS preflight request with Origin header, the CORS middleware
|
||||
// SHALL return status 204 with all configured CORS headers (Allow-Methods,
|
||||
// Allow-Headers, Expose-Headers, Max-Age, Credentials).
|
||||
// ============================================================================
|
||||
fn test_property_2_cors_preflight_response() bool {
|
||||
rand.seed([u32(time.now().unix()), u32(54321)])
|
||||
|
||||
for _ in 0 .. test_iterations {
|
||||
origin := generate_cors_random_origin()
|
||||
allow_methods := ['GET', 'POST', 'PUT', 'DELETE']
|
||||
allow_headers := generate_cors_random_headers()
|
||||
expose_headers := ['X-Custom-Response', 'X-Request-ID']
|
||||
max_age := rand.int_in_range(60, 86400) or { 3600 }
|
||||
credentials := rand.int_in_range(0, 2) or { 0 } == 1
|
||||
|
||||
mut ctx := create_cors_preflight_context(origin, allow_headers.join(', '))
|
||||
|
||||
cors_mw := hono.cors(hono.CorsOptions{
|
||||
origin: '*'
|
||||
allow_methods: allow_methods
|
||||
allow_headers: allow_headers
|
||||
expose_headers: expose_headers
|
||||
max_age: max_age
|
||||
credentials: credentials
|
||||
})
|
||||
|
||||
response := cors_mw(mut ctx, cors_mock_next)
|
||||
|
||||
// Check status code is 204
|
||||
if response.status_code != 204 {
|
||||
println(' Preflight status test failed: expected 204, got ${response.status_code}')
|
||||
return false
|
||||
}
|
||||
|
||||
// Check Access-Control-Allow-Origin
|
||||
allowed_origin := ctx.headers['Access-Control-Allow-Origin'] or { '' }
|
||||
if allowed_origin != '*' {
|
||||
println(' Preflight origin test failed: expected "*", got "${allowed_origin}"')
|
||||
return false
|
||||
}
|
||||
|
||||
// Check Access-Control-Allow-Methods
|
||||
allowed_methods := ctx.headers['Access-Control-Allow-Methods'] or { '' }
|
||||
if allowed_methods != allow_methods.join(', ') {
|
||||
println(' Preflight methods test failed: expected "${allow_methods.join(', ')}", got "${allowed_methods}"')
|
||||
return false
|
||||
}
|
||||
|
||||
// Check Access-Control-Allow-Headers
|
||||
result_headers := ctx.headers['Access-Control-Allow-Headers'] or { '' }
|
||||
if result_headers != allow_headers.join(', ') {
|
||||
println(' Preflight headers test failed: expected "${allow_headers.join(', ')}", got "${result_headers}"')
|
||||
return false
|
||||
}
|
||||
|
||||
// Check Access-Control-Expose-Headers
|
||||
result_expose := ctx.headers['Access-Control-Expose-Headers'] or { '' }
|
||||
if result_expose != expose_headers.join(', ') {
|
||||
println(' Preflight expose headers test failed: expected "${expose_headers.join(', ')}", got "${result_expose}"')
|
||||
return false
|
||||
}
|
||||
|
||||
// Check Access-Control-Max-Age
|
||||
result_max_age := ctx.headers['Access-Control-Max-Age'] or { '' }
|
||||
if result_max_age != max_age.str() {
|
||||
println(' Preflight max-age test failed: expected "${max_age}", got "${result_max_age}"')
|
||||
return false
|
||||
}
|
||||
|
||||
// Check Access-Control-Allow-Credentials
|
||||
result_credentials := ctx.headers['Access-Control-Allow-Credentials'] or { '' }
|
||||
if credentials {
|
||||
if result_credentials != 'true' {
|
||||
println(' Preflight credentials test failed: expected "true", got "${result_credentials}"')
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if result_credentials != '' {
|
||||
println(' Preflight credentials test failed: expected empty, got "${result_credentials}"')
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println('🚀 开始 CORS Middleware 属性测试...')
|
||||
println('每个属性测试运行 ${test_iterations} 次迭代\n')
|
||||
|
||||
mut stats := CorsTestStats{}
|
||||
|
||||
// 运行属性测试
|
||||
// Feature: builtin-middleware, Property 1: CORS Origin Header Consistency
|
||||
// Validates: Requirements 1.1, 1.3, 1.4, 1.5, 1.6
|
||||
stats.run_property_test('Property 1: CORS Origin Header Consistency', test_property_1_cors_origin_header_consistency)
|
||||
|
||||
// Feature: builtin-middleware, Property 2: CORS Preflight Response
|
||||
// Validates: Requirements 1.2, 1.7, 1.8, 1.9, 1.10, 1.11
|
||||
stats.run_property_test('Property 2: CORS Preflight Response', test_property_2_cors_preflight_response)
|
||||
|
||||
// 打印测试总结
|
||||
stats.print_summary()
|
||||
}
|
||||
Reference in New Issue
Block a user