package fjord import "core:fmt" import "core:log" import "core:net" import "core:os/os2" import "core:path/slashpath" import "core:strings" import "core:thread" import "core:time" import "core:testing" import http "http" StandardError :: enum { TestError, } Error :: union { StandardError, } LOCAL_ADDRESS :: net.IP4_Address{127, 0, 0, 1} @(private = "file") port_counter := 50000 @(private = "file") get_test_port :: proc() -> int { port := port_counter port_counter += 10 return port } first_handler :: proc(request: ^http.Request) -> (http.Response, Error) { entity := request.path_variables["entity"] body := strings.concatenate( {"
Hello first ", entity, "
"}, context.temp_allocator, ) response := http.make_response( .Ok, transmute([]byte)body, .Html, context.temp_allocator, ) return response, nil } second_handler :: proc(request: ^http.Request) -> (http.Response, Error) { entity := request.path_variables["entity"] body := strings.concatenate( {"
Hello second ", entity, "
"}, context.temp_allocator, ) response := http.make_response( .Ok, transmute([]byte)body, .Html, context.temp_allocator, ) return response, nil } third_handler :: proc(request: ^http.Request) -> (http.Response, Error) { entity := request.path_variables["entity"] other_entity := request.path_variables["other_entity"] body := strings.concatenate( {"
Hello third ", entity, " and ", other_entity, "
"}, context.temp_allocator, ) response := http.make_response( .Ok, transmute([]byte)body, .Html, context.temp_allocator, ) return response, nil } @(test) test_router_ok :: proc(t: ^testing.T) { context.logger = log.create_console_logger(.Info) defer log.destroy_console_logger(context.logger) endpoint := net.Endpoint { address = LOCAL_ADDRESS, port = get_test_port(), } THREAD_COUNT :: 8 server: Server(Error) server_init(&server, endpoint, THREAD_COUNT, context.allocator) defer server_destroy(&server) server_add_route(&server, .GET, {"hello", ":entity"}, first_handler) server_add_route( &server, .GET, {"hello", ":entity", "only"}, second_handler, ) server_add_route( &server, .GET, {"hello", ":entity", "and", ":other_entity"}, third_handler, ) handler, path_vars, ok := router_lookup( &server.router, .GET, {"hello", "world"}, context.temp_allocator, ); assert(ok) assert(handler == first_handler) assert(path_vars["entity"] == "world") handler, path_vars, ok = router_lookup( &server.router, .GET, {"hello", "lonely world", "only"}, context.temp_allocator, ); assert(ok) assert(handler == second_handler) assert(path_vars["entity"] == "lonely world") handler, path_vars, ok = router_lookup( &server.router, .GET, {"hello", "world", "and", "worlds friend"}, context.temp_allocator, ); assert(ok) assert(handler == third_handler) assert(path_vars["entity"] == "world") assert(path_vars["other_entity"] == "worlds friend") ok = server_remove_route(&server, .GET, {"hello", ":entity"}); assert(ok) ok = server_remove_route(&server, .GET, {"hello", ":entity"}); assert(!ok) free_all(context.temp_allocator) } start_concurrent_server :: proc( server: ^Server($Error_Type), ) -> ^thread.Thread { ServerThreadData :: struct { server: ^Server(Error_Type), thread: ^thread.Thread, } server_thread_proc :: proc(t: ^thread.Thread) { d := (^ServerThreadData)(t.data) server_listen_and_serve(d.server) } d := new(ServerThreadData, context.temp_allocator) d.server = server if d.thread = thread.create(server_thread_proc); d.thread != nil { d.thread.init_context = context d.thread.data = rawptr(d) thread.start(d.thread) } // Give server some time to start time.sleep(50 * time.Millisecond) return d.thread } shutdown_concurrent_server :: proc( server: ^Server($Error_Type), t: ^thread.Thread, endpoint: net.Endpoint, ) { server_shutdown(server) // Send a dummy request to unblock accept_tcp, todo: fix this immidiate server shutdown url := fmt.tprintf( "http://%d.%d.%d.%d:%d/dummy", endpoint.address.(net.IP4_Address)[0], endpoint.address.(net.IP4_Address)[1], endpoint.address.(net.IP4_Address)[2], endpoint.address.(net.IP4_Address)[3], endpoint.port, ) _, _, _, err := os2.process_exec( {command = []string{"curl", "-s", url}}, context.temp_allocator, ) assert(err == nil) thread.join(t) thread.destroy(t) } assert_endpoint_response :: proc( endpoint: net.Endpoint, path: []string, expected_response: string, ) { // Build URL from endpoint and path path_str := slashpath.join(path, context.temp_allocator) url := fmt.tprintf( "http://%d.%d.%d.%d:%d/%s", endpoint.address.(net.IP4_Address)[0], endpoint.address.(net.IP4_Address)[1], endpoint.address.(net.IP4_Address)[2], endpoint.address.(net.IP4_Address)[3], endpoint.port, path_str, ) state, stdout, stderr, err := os2.process_exec( {command = []string{"curl", "-s", url}}, context.temp_allocator, ) if err != nil { log.errorf("Failed to execute curl: %d %s %s", err, stdout, stderr) assert(false, "curl execution failed") return } if state.exit_code != 0 { log.errorf("curl failed with exit code: %d", state.exit_code) assert(false, "curl returned non-zero exit code") return } response := string(stdout) log.assertf( response == expected_response, "Expected: %s, but got: %s", response, expected_response, ) } @(test) test_server_general_ok :: proc(t: ^testing.T) { context.logger = log.create_console_logger(.Info) defer log.destroy_console_logger(context.logger) port := get_test_port() endpoint := net.Endpoint { address = LOCAL_ADDRESS, port = port, } THREAD_COUNT :: 8 server: Server(Error) server_init(&server, endpoint, THREAD_COUNT, context.allocator) defer server_destroy(&server) server_add_route(&server, .GET, {"hello", ":entity"}, first_handler) server_add_route( &server, .GET, {"hello", ":entity", "only"}, second_handler, ) server_add_route( &server, .GET, {"hello", ":entity", "and", ":other_entity"}, third_handler, ) t := start_concurrent_server(&server) // Try with some load ITERATIONS :: 100 for i in 0 ..< ITERATIONS { assert_endpoint_response( endpoint, {"hello", "world"}, "
Hello first world
", ) assert_endpoint_response( endpoint, {"hello", "lonely%20world", "only"}, "
Hello second lonely%20world
", ) assert_endpoint_response( endpoint, {"hello", "world", "and", "worlds%20friend"}, "
Hello third world and worlds%20friend
", ) } shutdown_concurrent_server(&server, t, endpoint) free_all(context.temp_allocator) } @(test) test_custom_not_found_handler :: proc(t: ^testing.T) { context.logger = log.create_console_logger(.Info) defer log.destroy_console_logger(context.logger) port := get_test_port() endpoint := net.Endpoint { address = LOCAL_ADDRESS, port = port, } THREAD_COUNT :: 8 server: Server(Error) server_init(&server, endpoint, THREAD_COUNT, context.allocator) defer server_destroy(&server) custom_not_found_handler :: proc( request: ^http.Request, ) -> ( http.Response, Error, ) { body := "
Custom 404
" http_response := http.make_response( .NotFound, transmute([]byte)body, .Html, ) return http_response, nil } server_set_not_found_handler(&server, custom_not_found_handler) server_add_route( &server, .GET, {"exists"}, proc(request: ^http.Request) -> (http.Response, Error) { body := "
This route exists
" response := http.make_response( .Ok, transmute([]byte)body, .Html, context.temp_allocator, ) return response, nil }, ) t := start_concurrent_server(&server) assert_endpoint_response( endpoint, {"does-not-exist"}, "
Custom 404
", ) assert_endpoint_response( endpoint, {"exists"}, "
This route exists
", ) shutdown_concurrent_server(&server, t, endpoint) free_all(context.temp_allocator) } @(test) test_custom_error_handler :: proc(t: ^testing.T) { context.logger = log.create_console_logger(.Info) defer log.destroy_console_logger(context.logger) port := get_test_port() endpoint := net.Endpoint { address = LOCAL_ADDRESS, port = port, } THREAD_COUNT :: 8 server: Server(Error) server_init(&server, endpoint, THREAD_COUNT, context.allocator) defer server_destroy(&server) custom_error_handler :: proc(error: Error) -> http.Response { body := "
Custom Error Handler
" http_response := http.make_response( .BadRequest, transmute([]byte)body, .Html, ) return http_response } server_set_error_handler(&server, custom_error_handler) error_handler :: proc(request: ^http.Request) -> (http.Response, Error) { return http.Response{}, .TestError } server_add_route(&server, .GET, {"error"}, error_handler) t := start_concurrent_server(&server) assert_endpoint_response( endpoint, {"error"}, "
Custom Error Handler
", ) shutdown_concurrent_server(&server, t, endpoint) free_all(context.temp_allocator) }