| 1 | // vtest retry: 3 |
| 2 | // vtest build: !sanitized_job? && !windows // !windows: fasthttp.Server.run not implemented yet |
| 3 | import os |
| 4 | import time |
| 5 | import x.json2 as json |
| 6 | import net |
| 7 | import net.http |
| 8 | import io |
| 9 | |
| 10 | const sport = 13005 |
| 11 | const localserver = '127.0.0.1:${sport}' |
| 12 | const exit_after_time = 12000 |
| 13 | // milliseconds |
| 14 | const vexe = os.getenv('VEXE') |
| 15 | const veb_logfile = os.getenv('VEB_LOGFILE') |
| 16 | const vroot = os.dir(vexe) |
| 17 | const serverexe = os.join_path(os.cache_dir(), 'veb_test_server.exe') |
| 18 | const tcp_r_timeout = 10 * time.second |
| 19 | const tcp_w_timeout = 10 * time.second |
| 20 | |
| 21 | // setup of veb webserver |
| 22 | fn testsuite_begin() { |
| 23 | os.chdir(vroot) or {} |
| 24 | if os.exists(serverexe) { |
| 25 | os.rm(serverexe) or {} |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | fn test_simple_veb_app_can_be_compiled() { |
| 30 | // did_server_compile := os.system('${os.quoted_path(vexe)} -g -o ${os.quoted_path(serverexe)} vlib/veb/tests/veb_test_server.v') |
| 31 | did_server_compile := |
| 32 | os.system('${os.quoted_path(vexe)} -o ${os.quoted_path(serverexe)} vlib/veb/tests/veb_test_server.v') |
| 33 | assert did_server_compile == 0 |
| 34 | assert os.exists(serverexe) |
| 35 | } |
| 36 | |
| 37 | fn test_a_simple_veb_app_runs_in_the_background() { |
| 38 | mut suffix := '' |
| 39 | $if !windows { |
| 40 | suffix = ' > /dev/null &' |
| 41 | } |
| 42 | if veb_logfile != '' { |
| 43 | suffix = ' 2>> ${os.quoted_path(veb_logfile)} >> ${os.quoted_path(veb_logfile)} &' |
| 44 | } |
| 45 | server_exec_cmd := '${os.quoted_path(serverexe)} ${sport} ${exit_after_time} ${suffix}' |
| 46 | $if debug_net_socket_client ? { |
| 47 | eprintln('running:\n${server_exec_cmd}') |
| 48 | } |
| 49 | $if windows { |
| 50 | spawn os.system(server_exec_cmd) |
| 51 | } $else { |
| 52 | res := os.system(server_exec_cmd) |
| 53 | assert res == 0 |
| 54 | } |
| 55 | $if macos { |
| 56 | time.sleep(1000 * time.millisecond) |
| 57 | } $else { |
| 58 | time.sleep(100 * time.millisecond) |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | // web client tests follow |
| 63 | fn assert_common_headers(received string) { |
| 64 | assert received.starts_with('HTTP/1.1 200 OK\r\n'), received |
| 65 | assert received.contains('Server: veb\r\n'), received |
| 66 | assert received.contains('Content-Length:'), received |
| 67 | assert received.contains('Connection: close\r\n'), received |
| 68 | } |
| 69 | |
| 70 | fn test_a_simple_tcp_client_can_connect_to_the_veb_server() { |
| 71 | received := simple_tcp_client(path: '/') or { |
| 72 | assert err.msg() == '' |
| 73 | return |
| 74 | } |
| 75 | assert_common_headers(received) |
| 76 | assert received.contains('Content-Type: text/plain'), received |
| 77 | assert received.contains('Content-Length: 14'), received |
| 78 | assert received.ends_with('Welcome to veb'), received |
| 79 | } |
| 80 | |
| 81 | fn test_a_simple_tcp_client_simple_route() { |
| 82 | received := simple_tcp_client(path: '/simple') or { |
| 83 | assert err.msg() == '' |
| 84 | return |
| 85 | } |
| 86 | assert_common_headers(received) |
| 87 | assert received.contains('Content-Type: text/plain') |
| 88 | assert received.contains('Content-Length: 15') |
| 89 | assert received.ends_with('A simple result') |
| 90 | } |
| 91 | |
| 92 | fn test_a_simple_tcp_client_zero_content_length() { |
| 93 | // tests that sending a content-length header of 0 doesn't hang on a read timeout |
| 94 | watch := time.new_stopwatch(auto_start: true) |
| 95 | simple_tcp_client(path: '/', headers: 'Content-Length: 0\r\n\r\n') or { |
| 96 | assert err.msg() == '' |
| 97 | return |
| 98 | } |
| 99 | assert watch.elapsed() < 1 * time.second |
| 100 | } |
| 101 | |
| 102 | fn test_timeout_after_delayed_body() { |
| 103 | // content length is 10, but we don't send anything. The request should timeout, |
| 104 | // but not error. |
| 105 | watch := time.new_stopwatch(auto_start: true) |
| 106 | res := simple_tcp_client( |
| 107 | path: '/json_echo' |
| 108 | headers: 'Content-Length: 10\r\n\r\n' |
| 109 | method_str: 'POST' |
| 110 | ) or { |
| 111 | assert err.msg() == '' |
| 112 | return |
| 113 | } |
| 114 | |
| 115 | assert res.ends_with('408 Request Timeout') |
| 116 | } |
| 117 | |
| 118 | fn test_a_simple_tcp_client_html_page() { |
| 119 | received := simple_tcp_client(path: '/html_page') or { |
| 120 | assert err.msg() == '' |
| 121 | return |
| 122 | } |
| 123 | assert_common_headers(received) |
| 124 | assert received.contains('Content-Type: text/html') |
| 125 | assert received.ends_with('<h1>ok</h1>') |
| 126 | } |
| 127 | |
| 128 | // net.http client based tests follow: |
| 129 | fn assert_common_http_headers(x http.Response) ! { |
| 130 | assert x.status() == .ok |
| 131 | assert x.header.get(.server)! == 'veb' |
| 132 | assert x.header.get(.content_length)!.int() > 0 |
| 133 | } |
| 134 | |
| 135 | fn test_http_client_index() { |
| 136 | x := http.get('http://${localserver}/') or { panic(err) } |
| 137 | assert_common_http_headers(x)! |
| 138 | assert x.header.get(.content_type)! == 'text/plain' |
| 139 | assert x.body == 'Welcome to veb' |
| 140 | assert x.header.get(.connection)! == 'close' |
| 141 | } |
| 142 | |
| 143 | fn test_http_client_404() { |
| 144 | server := 'http://${localserver}' |
| 145 | url_404_list := [ |
| 146 | '/zxcnbnm', |
| 147 | '/JHKAJA', |
| 148 | '/unknown', |
| 149 | ] |
| 150 | for url in url_404_list { |
| 151 | res := http.get('${server}${url}') or { panic(err) } |
| 152 | assert res.status() == .not_found |
| 153 | assert res.body == '404 on "${url}"' |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | fn test_http_client_simple() { |
| 158 | x := http.get('http://${localserver}/simple') or { panic(err) } |
| 159 | assert_common_http_headers(x)! |
| 160 | assert x.header.get(.content_type)! == 'text/plain' |
| 161 | assert x.body == 'A simple result' |
| 162 | } |
| 163 | |
| 164 | fn test_http_client_html_page() { |
| 165 | x := http.get('http://${localserver}/html_page') or { panic(err) } |
| 166 | assert_common_http_headers(x)! |
| 167 | assert x.header.get(.content_type)! == 'text/html' |
| 168 | assert x.body == '<h1>ok</h1>' |
| 169 | } |
| 170 | |
| 171 | fn test_http_client_settings_page() { |
| 172 | x := http.get('http://${localserver}/bilbo/settings') or { panic(err) } |
| 173 | assert_common_http_headers(x)! |
| 174 | assert x.body == 'username: bilbo' |
| 175 | |
| 176 | y := http.get('http://${localserver}/kent/settings') or { panic(err) } |
| 177 | assert_common_http_headers(y)! |
| 178 | assert y.body == 'username: kent' |
| 179 | } |
| 180 | |
| 181 | fn test_http_client_user_repo_settings_page() { |
| 182 | x := http.get('http://${localserver}/bilbo/gostamp/settings') or { panic(err) } |
| 183 | assert_common_http_headers(x)! |
| 184 | assert x.body == 'username: bilbo | repository: gostamp' |
| 185 | |
| 186 | y := http.get('http://${localserver}/kent/golang/settings') or { panic(err) } |
| 187 | assert_common_http_headers(y)! |
| 188 | assert y.body == 'username: kent | repository: golang' |
| 189 | |
| 190 | z := http.get('http://${localserver}/missing/golang/settings') or { panic(err) } |
| 191 | assert z.status() == .not_found |
| 192 | } |
| 193 | |
| 194 | struct User { |
| 195 | name string |
| 196 | age int |
| 197 | } |
| 198 | |
| 199 | fn test_http_client_json_post() { |
| 200 | ouser := User{ |
| 201 | name: 'Bilbo' |
| 202 | age: 123 |
| 203 | } |
| 204 | json_for_ouser := json.encode(ouser) |
| 205 | mut x := http.post_json('http://${localserver}/json_echo', json_for_ouser) or { panic(err) } |
| 206 | $if debug_net_socket_client ? { |
| 207 | eprintln('/json_echo endpoint response: ${x}') |
| 208 | } |
| 209 | assert x.header.get(.content_type)! == 'application/json' |
| 210 | assert x.body == json_for_ouser |
| 211 | nuser := json.decode[User](x.body) or { User{} } |
| 212 | assert '${ouser}' == '${nuser}' |
| 213 | |
| 214 | x = http.post_json('http://${localserver}/json', json_for_ouser) or { panic(err) } |
| 215 | $if debug_net_socket_client ? { |
| 216 | eprintln('/json endpoint response: ${x}') |
| 217 | } |
| 218 | assert x.header.get(.content_type)! == 'application/json' |
| 219 | assert x.body == json_for_ouser |
| 220 | nuser2 := json.decode[User](x.body) or { User{} } |
| 221 | assert '${ouser}' == '${nuser2}' |
| 222 | } |
| 223 | |
| 224 | fn test_http_client_multipart_form_data() { |
| 225 | mut form_config := http.PostMultipartFormConfig{ |
| 226 | form: { |
| 227 | 'foo': 'baz buzz' |
| 228 | } |
| 229 | } |
| 230 | |
| 231 | mut x := http.post_multipart_form('http://${localserver}/form_echo', form_config)! |
| 232 | |
| 233 | $if debug_net_socket_client ? { |
| 234 | eprintln('/form_echo endpoint response: ${x}') |
| 235 | } |
| 236 | assert x.body == form_config.form['foo'] |
| 237 | |
| 238 | mut files := []http.FileData{} |
| 239 | files << http.FileData{ |
| 240 | filename: 'veb' |
| 241 | content_type: 'text' |
| 242 | data: '"veb test"' |
| 243 | } |
| 244 | |
| 245 | mut form_config_files := http.PostMultipartFormConfig{ |
| 246 | files: { |
| 247 | 'file': files |
| 248 | } |
| 249 | } |
| 250 | |
| 251 | x = http.post_multipart_form('http://${localserver}/file_echo', form_config_files)! |
| 252 | $if debug_net_socket_client ? { |
| 253 | eprintln('/form_echo endpoint response: ${x}') |
| 254 | } |
| 255 | assert x.body == files[0].data |
| 256 | } |
| 257 | |
| 258 | fn test_login_with_multipart_form_data_send_by_fetch() { |
| 259 | mut form_config := http.PostMultipartFormConfig{ |
| 260 | form: { |
| 261 | 'username': 'myusername' |
| 262 | 'password': 'mypassword123' |
| 263 | } |
| 264 | } |
| 265 | x := http.post_multipart_form('http://${localserver}/login', form_config)! |
| 266 | assert x.status_code == 200 |
| 267 | assert x.status_msg == 'OK' |
| 268 | assert x.body == 'username: xmyusernamex | password: xmypassword123x' |
| 269 | } |
| 270 | |
| 271 | fn test_query_params_are_passed_as_arguments() { |
| 272 | x := http.get('http://${localserver}/query_echo?c=3&a="test"&b=20')! |
| 273 | assert x.status() == .ok |
| 274 | assert x.body == 'a: x"test"x | b: x20x' |
| 275 | } |
| 276 | |
| 277 | fn test_host() { |
| 278 | mut req := http.Request{ |
| 279 | url: 'http://${localserver}/with_host' |
| 280 | method: .get |
| 281 | } |
| 282 | |
| 283 | mut x := req.do()! |
| 284 | assert x.status() == .not_found |
| 285 | |
| 286 | req.add_header(.host, 'example.com') |
| 287 | x = req.do()! |
| 288 | assert x.status() == .ok |
| 289 | } |
| 290 | |
| 291 | fn test_empty_response_body_has_content_length() { |
| 292 | req := http.Request{ |
| 293 | url: 'http://${localserver}/empty_response_body' |
| 294 | method: .get |
| 295 | } |
| 296 | |
| 297 | mut x := req.do()! |
| 298 | assert x.status() == .ok |
| 299 | assert x.header.get(.content_length)! == '0' |
| 300 | } |
| 301 | |
| 302 | fn test_http_client_shutdown_does_not_work_without_a_cookie() { |
| 303 | x := http.get('http://${localserver}/shutdown') or { |
| 304 | assert err.msg() == '' |
| 305 | return |
| 306 | } |
| 307 | assert x.status() == .not_found |
| 308 | } |
| 309 | |
| 310 | fn testsuite_end() { |
| 311 | // This test is guaranteed to be called last. |
| 312 | // It sends a request to the server to shutdown. |
| 313 | x := http.fetch( |
| 314 | url: 'http://${localserver}/shutdown' |
| 315 | method: .get |
| 316 | cookies: { |
| 317 | 'skey': 'superman' |
| 318 | } |
| 319 | ) or { |
| 320 | assert err.msg() == '' |
| 321 | return |
| 322 | } |
| 323 | assert x.status() == .ok |
| 324 | assert x.body == 'good bye' |
| 325 | } |
| 326 | |
| 327 | // utility code: |
| 328 | struct SimpleTcpClientConfig { |
| 329 | retries int = 4 |
| 330 | host string = 'static.dev' |
| 331 | path string = '/' |
| 332 | agent string = 'v/net.tcp.v' |
| 333 | headers string = '\r\n' |
| 334 | content string |
| 335 | method_str string = 'GET' |
| 336 | } |
| 337 | |
| 338 | fn simple_tcp_client(config SimpleTcpClientConfig) !string { |
| 339 | mut client := &net.TcpConn(unsafe { nil }) |
| 340 | mut tries := 0 |
| 341 | for tries < config.retries { |
| 342 | tries++ |
| 343 | eprintln('> client retries: ${tries}') |
| 344 | client = net.dial_tcp(localserver) or { |
| 345 | eprintln('dial error: ${err.msg()}') |
| 346 | if tries > config.retries { |
| 347 | return err |
| 348 | } |
| 349 | time.sleep(100 * time.millisecond) |
| 350 | continue |
| 351 | } |
| 352 | break |
| 353 | } |
| 354 | if client == unsafe { nil } { |
| 355 | eprintln('could not create a tcp client connection to http://${localserver} after ${config.retries} retries') |
| 356 | exit(1) |
| 357 | } |
| 358 | client.set_read_timeout(tcp_r_timeout) |
| 359 | client.set_write_timeout(tcp_w_timeout) |
| 360 | defer { |
| 361 | client.close() or {} |
| 362 | } |
| 363 | message := '${config.method_str} ${config.path} HTTP/1.1 |
| 364 | Host: ${config.host} |
| 365 | User-Agent: ${config.agent} |
| 366 | Accept: */* |
| 367 | Connection: close |
| 368 | ${config.headers} |
| 369 | ${config.content}' |
| 370 | $if debug_net_socket_client ? { |
| 371 | eprintln('sending:\n${message}') |
| 372 | } |
| 373 | client.write(message.bytes())! |
| 374 | read := io.read_all(reader: client)! |
| 375 | $if debug_net_socket_client ? { |
| 376 | eprintln('received:\n${read}') |
| 377 | } |
| 378 | return read.bytestr() |
| 379 | } |
| 380 | |
| 381 | // for issue 20476 |
| 382 | // phenomenon: parsing url error when querypath is `//` |
| 383 | fn test_empty_querypath() { |
| 384 | mut x := http.get('http://${localserver}') or { panic(err) } |
| 385 | assert x.body == 'Welcome to veb' |
| 386 | x = http.get('http://${localserver}/') or { panic(err) } |
| 387 | assert x.body == 'Welcome to veb' |
| 388 | x = http.get('http://${localserver}//') or { panic(err) } |
| 389 | assert x.body == 'Welcome to veb' |
| 390 | x = http.get('http://${localserver}///') or { panic(err) } |
| 391 | assert x.body == 'Welcome to veb' |
| 392 | } |
| 393 | |
| 394 | fn test_large_response() { |
| 395 | received := simple_tcp_client(path: '/large_response') or { panic(err) } |
| 396 | assert_common_headers(received) |
| 397 | assert received.ends_with('}]') |
| 398 | assert received.len == 830778 |
| 399 | } |
| 400 | |