v / vlib / fasthttp / fasthttp_test.v
237 lines · 206 sloc · 6.37 KB · e632a84cd573bb05f3f72a0ae0cb9bbcaae404da
Raw
1module fasthttp
2
3import net
4import os
5import time
6
7const fasthttp_example_exe = os.join_path(os.cache_dir(), 'fasthttp_example_test.exe')
8const reusable_takeover_port = 13019
9const reusable_takeover_addr = '127.0.0.1:${reusable_takeover_port}'
10
11fn 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
18fn 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
29fn 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
54fn 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
66fn 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
81fn 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
91fn 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
102fn 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
120fn 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
153fn 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
198fn 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
213fn 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
223fn 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