| 1 | module fasthttp |
| 2 | |
| 3 | import net |
| 4 | import os |
| 5 | import time |
| 6 | |
| 7 | const fasthttp_example_exe = os.join_path(os.cache_dir(), 'fasthttp_example_test.exe') |
| 8 | const reusable_takeover_port = 13019 |
| 9 | const reusable_takeover_addr = '127.0.0.1:${reusable_takeover_port}' |
| 10 | |
| 11 | fn testsuite_begin() { |
| 12 | // Clean up old example binary if it exists |
| 13 | if os.exists(fasthttp_example_exe) { |
| 14 | os.rm(fasthttp_example_exe) or {} |
| 15 | } |
| 16 | } |
| 17 | |
| 18 | fn test_fasthttp_example_compiles() { |
| 19 | vexe := os.getenv('VEXE') |
| 20 | vroot := os.dir(vexe) |
| 21 | |
| 22 | // Build the fasthttp example |
| 23 | build_result := os.system('${os.quoted_path(vexe)} -o ${os.quoted_path(fasthttp_example_exe)} ${os.join_path(vroot, |
| 24 | 'examples', 'fasthttp')}') |
| 25 | assert build_result == 0, 'fasthttp example failed to compile' |
| 26 | assert os.exists(fasthttp_example_exe), 'fasthttp example binary not found after build' |
| 27 | } |
| 28 | |
| 29 | fn test_parse_request_line() { |
| 30 | // Test basic GET request |
| 31 | request := 'GET / HTTP/1.1\r\n'.bytes() |
| 32 | req := decode_http_request(request) or { |
| 33 | assert false, 'Failed to parse valid request: ${err}' |
| 34 | return |
| 35 | } |
| 36 | |
| 37 | assert req.buffer.len == request.len |
| 38 | assert req.method.start == 0 |
| 39 | assert req.method.len == 3 |
| 40 | assert req.path.start == 4 |
| 41 | assert req.path.len == 1 |
| 42 | assert req.version.start == 6 |
| 43 | assert req.version.len == 8 |
| 44 | |
| 45 | method := req.buffer[req.method.start..req.method.start + req.method.len].bytestr() |
| 46 | path := req.buffer[req.path.start..req.path.start + req.path.len].bytestr() |
| 47 | version := req.buffer[req.version.start..req.version.start + req.version.len].bytestr() |
| 48 | |
| 49 | assert method == 'GET' |
| 50 | assert path == '/' |
| 51 | assert version == 'HTTP/1.1' |
| 52 | } |
| 53 | |
| 54 | fn test_parse_request_line_with_path() { |
| 55 | // Test GET request with path |
| 56 | request := 'GET /users/123 HTTP/1.1\r\n'.bytes() |
| 57 | req := decode_http_request(request) or { |
| 58 | assert false, 'Failed to parse valid request: ${err}' |
| 59 | return |
| 60 | } |
| 61 | |
| 62 | path := req.buffer[req.path.start..req.path.start + req.path.len].bytestr() |
| 63 | assert path == '/users/123' |
| 64 | } |
| 65 | |
| 66 | fn test_parse_request_line_post() { |
| 67 | // Test POST request |
| 68 | request := 'POST /api/data HTTP/1.1\r\n'.bytes() |
| 69 | req := decode_http_request(request) or { |
| 70 | assert false, 'Failed to parse valid request: ${err}' |
| 71 | return |
| 72 | } |
| 73 | |
| 74 | method := req.buffer[req.method.start..req.method.start + req.method.len].bytestr() |
| 75 | path := req.buffer[req.path.start..req.path.start + req.path.len].bytestr() |
| 76 | |
| 77 | assert method == 'POST' |
| 78 | assert path == '/api/data' |
| 79 | } |
| 80 | |
| 81 | fn test_parse_request_line_invalid() { |
| 82 | // Test invalid request (missing \r\n) |
| 83 | request := 'GET / HTTP/1.1'.bytes() |
| 84 | decode_http_request(request) or { |
| 85 | assert err.msg() == 'Invalid HTTP request line: Missing CR' |
| 86 | return |
| 87 | } |
| 88 | assert false, 'Should have failed to parse invalid request' |
| 89 | } |
| 90 | |
| 91 | fn test_decode_http_request() { |
| 92 | request := 'GET /test HTTP/1.1\r\n'.bytes() |
| 93 | req := decode_http_request(request) or { |
| 94 | assert false, 'Failed to decode request: ${err}' |
| 95 | return |
| 96 | } |
| 97 | |
| 98 | method := req.buffer[req.method.start..req.method.start + req.method.len].bytestr() |
| 99 | assert method == 'GET' |
| 100 | } |
| 101 | |
| 102 | fn test_new_server() { |
| 103 | handler := fn (req HttpRequest) !HttpResponse { |
| 104 | return HttpResponse{ |
| 105 | content: 'HTTP/1.1 200 OK\r\n\r\nHello'.bytes() |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | server := new_server(ServerConfig{ |
| 110 | port: 8080 |
| 111 | handler: handler |
| 112 | }) or { |
| 113 | assert false, 'Failed to create server: ${err}' |
| 114 | return |
| 115 | } |
| 116 | |
| 117 | assert server.port == 8080 |
| 118 | } |
| 119 | |
| 120 | fn test_server_ipv4_ipv6_binding() { |
| 121 | // Test IPv4 binding |
| 122 | handler := fn (req HttpRequest) !HttpResponse { |
| 123 | return HttpResponse{ |
| 124 | content: 'HTTP/1.1 200 OK\r\n\r\nIPv4 test'.bytes() |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | server_ipv4 := new_server(ServerConfig{ |
| 129 | family: .ip |
| 130 | port: 8081 |
| 131 | handler: handler |
| 132 | }) or { |
| 133 | assert false, 'Failed to create IPv4 server: ${err}' |
| 134 | return |
| 135 | } |
| 136 | |
| 137 | // Test IPv6 binding |
| 138 | server_ipv6 := new_server(ServerConfig{ |
| 139 | family: .ip6 |
| 140 | port: 8082 |
| 141 | handler: handler |
| 142 | }) or { |
| 143 | assert false, 'Failed to create IPv6 server: ${err}' |
| 144 | return |
| 145 | } |
| 146 | |
| 147 | // Verify both servers were created successfully |
| 148 | // Note: family field is not exported, so we can't directly test it |
| 149 | assert server_ipv4.port == 8081 |
| 150 | assert server_ipv6.port == 8082 |
| 151 | } |
| 152 | |
| 153 | fn test_response_takeover_mode_reusable_keeps_connection() { |
| 154 | $if linux || bsd { |
| 155 | mut server := new_server(ServerConfig{ |
| 156 | family: .ip |
| 157 | port: reusable_takeover_port |
| 158 | timeout_in_seconds: 2 |
| 159 | max_request_buffer_size: 8192 |
| 160 | handler: reusable_takeover_handler |
| 161 | }) or { |
| 162 | assert false, 'Failed to create server: ${err}' |
| 163 | return |
| 164 | } |
| 165 | handle := server.handle() |
| 166 | spawn server.run() |
| 167 | handle.wait_till_running(max_retries: 1000, retry_period_ms: 10) or { |
| 168 | assert false, 'server did not start: ${err}' |
| 169 | return |
| 170 | } |
| 171 | defer { |
| 172 | handle.shutdown(timeout: 5 * time.second) or {} |
| 173 | } |
| 174 | |
| 175 | mut conn := net.dial_tcp(reusable_takeover_addr)! |
| 176 | conn.set_read_timeout(2 * time.second) |
| 177 | conn.set_write_timeout(2 * time.second) |
| 178 | defer { |
| 179 | conn.close() or {} |
| 180 | } |
| 181 | |
| 182 | conn.write_string('GET /reus')! |
| 183 | time.sleep(50 * time.millisecond) |
| 184 | conn.write_string('able HTTP/1.1\r\nHost: ${reusable_takeover_addr}\r\n\r\n')! |
| 185 | reusable_response := read_until_contains(mut conn, '\r\n0\r\n\r\n')! |
| 186 | assert reusable_response.contains('manual') == true, reusable_response |
| 187 | assert reusable_response.contains('\r\n0\r\n\r\n') == true, reusable_response |
| 188 | |
| 189 | conn.write_string('GET /normal HTTP/1.1\r\nHost: ${reusable_takeover_addr}\r\n\r\n')! |
| 190 | normal_response := read_until_contains(mut conn, 'normal')! |
| 191 | assert normal_response.contains('normal') == true, normal_response |
| 192 | assert normal_response.contains('Connection: close') == false, normal_response |
| 193 | } $else { |
| 194 | return |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | fn reusable_takeover_handler(req HttpRequest) !HttpResponse { |
| 199 | path := req.buffer[req.path.start..req.path.start + req.path.len].bytestr() |
| 200 | if path == '/reusable' { |
| 201 | body := 'manual' |
| 202 | send_raw_response(req.client_conn_fd, |
| 203 | 'HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n${body.len:x}\r\n${body}\r\n0\r\n\r\n') |
| 204 | return HttpResponse{ |
| 205 | takeover_mode: .reusable |
| 206 | } |
| 207 | } |
| 208 | return HttpResponse{ |
| 209 | content: 'HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nnormal'.bytes() |
| 210 | } |
| 211 | } |
| 212 | |
| 213 | fn send_raw_response(fd int, response string) { |
| 214 | $if linux { |
| 215 | C.send(fd, response.str, response.len, C.MSG_NOSIGNAL) |
| 216 | } $else $if bsd { |
| 217 | C.send(fd, response.str, response.len, send_flags) |
| 218 | } $else { |
| 219 | C.send(fd, response.str, response.len, 0) |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | fn read_until_contains(mut conn net.TcpConn, marker string) !string { |
| 224 | mut raw := '' |
| 225 | mut buf := []u8{len: 1024} |
| 226 | for _ in 0 .. 16 { |
| 227 | n := conn.read(mut buf)! |
| 228 | if n <= 0 { |
| 229 | break |
| 230 | } |
| 231 | raw += buf[..n].bytestr() |
| 232 | if raw.contains(marker) { |
| 233 | break |
| 234 | } |
| 235 | } |
| 236 | return raw |
| 237 | } |
| 238 | |