1import re 2import time 3 4import pytest 5from packaging import version 6 7from unit.applications.lang.python import ApplicationPython 8 9prerequisites = { 10 'modules': {'python': lambda v: version.parse(v) >= version.parse('3.5')} 11} 12 13client = ApplicationPython(load_module='asgi') 14 15 16def test_asgi_application_variables(date_to_sec_epoch, sec_epoch): 17 client.load('variables') 18 19 body = 'Test body string.' 20 21 resp = client.http( 22 f"""POST / HTTP/1.1 23Host: localhost 24Content-Length: {len(body)} 25Custom-Header: blah 26Custom-hEader: Blah 27Content-Type: text/html 28Connection: close 29custom-header: BLAH 30 31{body}""".encode(), 32 raw=True, 33 ) 34 35 assert resp['status'] == 200, 'status' 36 headers = resp['headers'] 37 header_server = headers.pop('Server') 38 assert re.search(r'Unit/[\d\.]+', header_server), 'server header' 39 40 date = headers.pop('Date') 41 assert date[-4:] == ' GMT', 'date header timezone' 42 assert abs(date_to_sec_epoch(date) - sec_epoch) < 5, 'date header' 43 44 assert headers == { 45 'Connection': 'close', 46 'content-length': str(len(body)), 47 'content-type': 'text/html', 48 'request-method': 'POST', 49 'request-uri': '/', 50 'http-host': 'localhost', 51 'http-version': '1.1', 52 'custom-header': 'blah, Blah, BLAH', 53 'asgi-version': '3.0', 54 'asgi-spec-version': '2.1', 55 'scheme': 'http', 56 }, 'headers' 57 assert resp['body'] == body, 'body' 58 59 60def test_asgi_application_ipv6(): 61 client.load('empty') 62 63 assert 'success' in client.conf( 64 {"[::1]:8080": {"pass": "applications/empty"}}, 'listeners' 65 ) 66 67 assert client.get(sock_type='ipv6')['status'] == 200 68 69 70def test_asgi_application_unix(temp_dir): 71 client.load('empty') 72 73 addr = f'{temp_dir}/sock' 74 assert 'success' in client.conf( 75 {f"unix:{addr}": {"pass": "applications/empty"}}, 'listeners' 76 ) 77 78 assert client.get(sock_type='unix', addr=addr)['status'] == 200 79 80 81def test_asgi_application_query_string(): 82 client.load('query_string') 83 84 resp = client.get(url='/?var1=val1&var2=val2') 85 86 assert ( 87 resp['headers']['query-string'] == 'var1=val1&var2=val2' 88 ), 'query-string header' 89 90 91def test_asgi_application_prefix(): 92 client.load('prefix', prefix='/api/rest') 93 94 def set_prefix(prefix): 95 client.conf(f'"{prefix}"', 'applications/prefix/prefix') 96 97 def check_prefix(url, prefix): 98 resp = client.get(url=url) 99 assert resp['status'] == 200 100 assert resp['headers']['prefix'] == prefix 101 102 check_prefix('/ap', 'NULL') 103 check_prefix('/api', 'NULL') 104 check_prefix('/api/', 'NULL') 105 check_prefix('/api/res', 'NULL') 106 check_prefix('/api/restful', 'NULL') 107 check_prefix('/api/rest', '/api/rest') 108 check_prefix('/api/rest/', '/api/rest') 109 check_prefix('/api/rest/get', '/api/rest') 110 check_prefix('/api/rest/get/blah', '/api/rest') 111 112 set_prefix('/api/rest/') 113 check_prefix('/api/rest', '/api/rest') 114 check_prefix('/api/restful', 'NULL') 115 check_prefix('/api/rest/', '/api/rest') 116 check_prefix('/api/rest/blah', '/api/rest') 117 118 set_prefix('/app') 119 check_prefix('/ap', 'NULL') 120 check_prefix('/app', '/app') 121 check_prefix('/app/', '/app') 122 check_prefix('/application/', 'NULL') 123 124 set_prefix('/') 125 check_prefix('/', 'NULL') 126 check_prefix('/app', 'NULL') 127 128 129def test_asgi_application_query_string_space(): 130 client.load('query_string') 131 132 resp = client.get(url='/ ?var1=val1&var2=val2') 133 assert ( 134 resp['headers']['query-string'] == 'var1=val1&var2=val2' 135 ), 'query-string space' 136 137 resp = client.get(url='/ %20?var1=val1&var2=val2') 138 assert ( 139 resp['headers']['query-string'] == 'var1=val1&var2=val2' 140 ), 'query-string space 2' 141 142 resp = client.get(url='/ %20 ?var1=val1&var2=val2') 143 assert ( 144 resp['headers']['query-string'] == 'var1=val1&var2=val2' 145 ), 'query-string space 3' 146 147 resp = client.get(url='/blah %20 blah? var1= val1 & var2=val2') 148 assert ( 149 resp['headers']['query-string'] == ' var1= val1 & var2=val2' 150 ), 'query-string space 4' 151 152 153def test_asgi_application_query_string_empty(): 154 client.load('query_string') 155 156 resp = client.get(url='/?') 157 158 assert resp['status'] == 200, 'query string empty status' 159 assert resp['headers']['query-string'] == '', 'query string empty' 160 161 162def test_asgi_application_query_string_absent(): 163 client.load('query_string') 164 165 resp = client.get() 166 167 assert resp['status'] == 200, 'query string absent status' 168 assert resp['headers']['query-string'] == '', 'query string absent' 169 170 171@pytest.mark.skip('not yet') 172def test_asgi_application_server_port(): 173 client.load('server_port') 174 175 assert ( 176 client.get()['headers']['Server-Port'] == '8080' 177 ), 'Server-Port header' 178 179 180@pytest.mark.skip('not yet') 181def test_asgi_application_working_directory_invalid(): 182 client.load('empty') 183 184 assert 'success' in client.conf( 185 '"/blah"', 'applications/empty/working_directory' 186 ), 'configure invalid working_directory' 187 188 assert client.get()['status'] == 500, 'status' 189 190 191def test_asgi_application_204_transfer_encoding(): 192 client.load('204_no_content') 193 194 assert ( 195 'Transfer-Encoding' not in client.get()['headers'] 196 ), '204 header transfer encoding' 197 198 199def test_asgi_application_shm_ack_handle(): 200 # Minimum possible limit 201 shm_limit = 10 * 1024 * 1024 202 203 client.load('mirror', limits={"shm": shm_limit}) 204 205 # Should exceed shm_limit 206 max_body_size = 12 * 1024 * 1024 207 208 assert 'success' in client.conf( 209 f'{{"http":{{"max_body_size": {max_body_size} }}}}', 210 'settings', 211 ) 212 213 assert client.get()['status'] == 200, 'init' 214 215 body = '0123456789AB' * 1024 * 1024 # 12 Mb 216 resp = client.post(body=body, read_buffer_size=1024 * 1024) 217 218 assert resp['body'] == body, 'keep-alive 1' 219 220 221def test_asgi_application_body_bytearray(): 222 client.load('body_bytearray') 223 224 body = '0123456789' 225 226 assert client.post(body=body)['body'] == body 227 228 229def test_asgi_keepalive_body(): 230 client.load('mirror') 231 232 assert client.get()['status'] == 200, 'init' 233 234 body = '0123456789' * 500 235 (resp, sock) = client.post( 236 headers={ 237 'Host': 'localhost', 238 'Connection': 'keep-alive', 239 }, 240 start=True, 241 body=body, 242 read_timeout=1, 243 ) 244 245 assert resp['body'] == body, 'keep-alive 1' 246 247 body = '0123456789' 248 resp = client.post(sock=sock, body=body) 249 250 assert resp['body'] == body, 'keep-alive 2' 251 252 253def test_asgi_keepalive_reconfigure(): 254 client.load('mirror') 255 256 assert client.get()['status'] == 200, 'init' 257 258 body = '0123456789' 259 conns = 3 260 socks = [] 261 262 for i in range(conns): 263 (resp, sock) = client.post( 264 headers={ 265 'Host': 'localhost', 266 'Connection': 'keep-alive', 267 }, 268 start=True, 269 body=body, 270 read_timeout=1, 271 ) 272 273 assert resp['body'] == body, 'keep-alive open' 274 275 client.load('mirror', processes=i + 1) 276 277 socks.append(sock) 278 279 for i in range(conns): 280 (resp, sock) = client.post( 281 headers={ 282 'Host': 'localhost', 283 'Connection': 'keep-alive', 284 }, 285 start=True, 286 sock=socks[i], 287 body=body, 288 read_timeout=1, 289 ) 290 291 assert resp['body'] == body, 'keep-alive request' 292 293 client.load('mirror', processes=i + 1) 294 295 for i in range(conns): 296 resp = client.post(sock=socks[i], body=body) 297 298 assert resp['body'] == body, 'keep-alive close' 299 300 client.load('mirror', processes=i + 1) 301 302 303def test_asgi_keepalive_reconfigure_2(): 304 client.load('mirror') 305 306 assert client.get()['status'] == 200, 'init' 307 308 body = '0123456789' 309 310 (resp, sock) = client.post( 311 headers={ 312 'Host': 'localhost', 313 'Connection': 'keep-alive', 314 }, 315 start=True, 316 body=body, 317 read_timeout=1, 318 ) 319 320 assert resp['body'] == body, 'reconfigure 2 keep-alive 1' 321 322 client.load('empty') 323 324 assert client.get()['status'] == 200, 'init' 325 326 (resp, sock) = client.post(start=True, sock=sock, body=body) 327 328 assert resp['status'] == 200, 'reconfigure 2 keep-alive 2' 329 assert resp['body'] == '', 'reconfigure 2 keep-alive 2 body' 330 331 assert 'success' in client.conf( 332 {"listeners": {}, "applications": {}} 333 ), 'reconfigure 2 clear configuration' 334 335 resp = client.get(sock=sock) 336 337 assert resp == {}, 'reconfigure 2 keep-alive 3' 338 339 340def test_asgi_keepalive_reconfigure_3(): 341 client.load('empty') 342 343 assert client.get()['status'] == 200, 'init' 344 345 sock = client.http( 346 b"""GET / HTTP/1.1 347""", 348 raw=True, 349 no_recv=True, 350 ) 351 352 assert client.get()['status'] == 200 353 354 assert 'success' in client.conf( 355 {"listeners": {}, "applications": {}} 356 ), 'reconfigure 3 clear configuration' 357 358 resp = client.http( 359 b"""Host: localhost 360Connection: close 361 362""", 363 sock=sock, 364 raw=True, 365 ) 366 367 assert resp['status'] == 200, 'reconfigure 3' 368 369 370def test_asgi_process_switch(): 371 client.load('delayed', processes=2) 372 373 client.get( 374 headers={ 375 'Host': 'localhost', 376 'Content-Length': '0', 377 'X-Delay': '5', 378 'Connection': 'close', 379 }, 380 no_recv=True, 381 ) 382 383 headers_delay_1 = { 384 'Connection': 'close', 385 'Host': 'localhost', 386 'Content-Length': '0', 387 'X-Delay': '1', 388 } 389 390 client.get(headers=headers_delay_1, no_recv=True) 391 392 time.sleep(0.5) 393 394 for _ in range(10): 395 client.get(headers=headers_delay_1, no_recv=True) 396 397 client.get(headers=headers_delay_1) 398 399 400def test_asgi_application_loading_error(skip_alert): 401 skip_alert(r'Python failed to import module "blah"') 402 403 client.load('empty', module="blah") 404 405 assert client.get()['status'] == 503, 'loading error' 406 407 408def test_asgi_application_threading(wait_for_record): 409 """wait_for_record() timeouts after 5s while every thread works at 410 least 3s. So without releasing GIL test should fail. 411 """ 412 413 client.load('threading') 414 415 for _ in range(10): 416 client.get(no_recv=True) 417 418 assert ( 419 wait_for_record(r'\(5\) Thread: 100', wait=50) is not None 420 ), 'last thread finished' 421 422 423def test_asgi_application_threads(): 424 client.load('threads', threads=2) 425 426 socks = [] 427 428 for _ in range(2): 429 sock = client.get( 430 headers={ 431 'Host': 'localhost', 432 'X-Delay': '3', 433 'Connection': 'close', 434 }, 435 no_recv=True, 436 ) 437 438 socks.append(sock) 439 440 time.sleep(1.0) # required to avoid greedy request reading 441 442 threads = set() 443 444 for sock in socks: 445 resp = client.recvall(sock).decode('utf-8') 446 447 client.log_in(resp) 448 449 resp = client._resp_to_dict(resp) 450 451 assert resp['status'] == 200, 'status' 452 453 threads.add(resp['headers']['x-thread']) 454 455 sock.close() 456 457 assert len(socks) == len(threads), 'threads differs' 458 459 460def test_asgi_application_legacy(): 461 client.load('legacy') 462 463 resp = client.get( 464 headers={ 465 'Host': 'localhost', 466 'Content-Length': '0', 467 'Connection': 'close', 468 }, 469 ) 470 471 assert resp['status'] == 200, 'status' 472 473 474def test_asgi_application_legacy_force(): 475 client.load('legacy_force', protocol='asgi') 476 477 resp = client.get( 478 headers={ 479 'Host': 'localhost', 480 'Content-Length': '0', 481 'Connection': 'close', 482 }, 483 ) 484 485 assert resp['status'] == 200, 'status' 486