import re import time import pytest from packaging import version from unit.applications.lang.python import ApplicationPython prerequisites = { 'modules': {'python': lambda v: version.parse(v) >= version.parse('3.5')} } client = ApplicationPython(load_module='asgi') def test_asgi_application_variables(date_to_sec_epoch, sec_epoch): client.load('variables') body = 'Test body string.' resp = client.http( f"""POST / HTTP/1.1 Host: localhost Content-Length: {len(body)} Custom-Header: blah Custom-hEader: Blah Content-Type: text/html Connection: close custom-header: BLAH {body}""".encode(), raw=True, ) assert resp['status'] == 200, 'status' headers = resp['headers'] header_server = headers.pop('Server') assert re.search(r'Unit/[\d\.]+', header_server), 'server header' date = headers.pop('Date') assert date[-4:] == ' GMT', 'date header timezone' assert abs(date_to_sec_epoch(date) - sec_epoch) < 5, 'date header' assert headers == { 'Connection': 'close', 'content-length': str(len(body)), 'content-type': 'text/html', 'request-method': 'POST', 'request-uri': '/', 'http-host': 'localhost', 'http-version': '1.1', 'custom-header': 'blah, Blah, BLAH', 'asgi-version': '3.0', 'asgi-spec-version': '2.1', 'scheme': 'http', }, 'headers' assert resp['body'] == body, 'body' def test_asgi_application_ipv6(): client.load('empty') assert 'success' in client.conf( {"[::1]:8080": {"pass": "applications/empty"}}, 'listeners' ) assert client.get(sock_type='ipv6')['status'] == 200 def test_asgi_application_unix(temp_dir): client.load('empty') addr = f'{temp_dir}/sock' assert 'success' in client.conf( {f"unix:{addr}": {"pass": "applications/empty"}}, 'listeners' ) assert client.get(sock_type='unix', addr=addr)['status'] == 200 def test_asgi_application_query_string(): client.load('query_string') resp = client.get(url='/?var1=val1&var2=val2') assert ( resp['headers']['query-string'] == 'var1=val1&var2=val2' ), 'query-string header' def test_asgi_application_prefix(): client.load('prefix', prefix='/api/rest') def set_prefix(prefix): client.conf(f'"{prefix}"', 'applications/prefix/prefix') def check_prefix(url, prefix): resp = client.get(url=url) assert resp['status'] == 200 assert resp['headers']['prefix'] == prefix check_prefix('/ap', 'NULL') check_prefix('/api', 'NULL') check_prefix('/api/', 'NULL') check_prefix('/api/res', 'NULL') check_prefix('/api/restful', 'NULL') check_prefix('/api/rest', '/api/rest') check_prefix('/api/rest/', '/api/rest') check_prefix('/api/rest/get', '/api/rest') check_prefix('/api/rest/get/blah', '/api/rest') set_prefix('/api/rest/') check_prefix('/api/rest', '/api/rest') check_prefix('/api/restful', 'NULL') check_prefix('/api/rest/', '/api/rest') check_prefix('/api/rest/blah', '/api/rest') set_prefix('/app') check_prefix('/ap', 'NULL') check_prefix('/app', '/app') check_prefix('/app/', '/app') check_prefix('/application/', 'NULL') set_prefix('/') check_prefix('/', 'NULL') check_prefix('/app', 'NULL') def test_asgi_application_query_string_space(): client.load('query_string') resp = client.get(url='/ ?var1=val1&var2=val2') assert ( resp['headers']['query-string'] == 'var1=val1&var2=val2' ), 'query-string space' resp = client.get(url='/ %20?var1=val1&var2=val2') assert ( resp['headers']['query-string'] == 'var1=val1&var2=val2' ), 'query-string space 2' resp = client.get(url='/ %20 ?var1=val1&var2=val2') assert ( resp['headers']['query-string'] == 'var1=val1&var2=val2' ), 'query-string space 3' resp = client.get(url='/blah %20 blah? var1= val1 & var2=val2') assert ( resp['headers']['query-string'] == ' var1= val1 & var2=val2' ), 'query-string space 4' def test_asgi_application_query_string_empty(): client.load('query_string') resp = client.get(url='/?') assert resp['status'] == 200, 'query string empty status' assert resp['headers']['query-string'] == '', 'query string empty' def test_asgi_application_query_string_absent(): client.load('query_string') resp = client.get() assert resp['status'] == 200, 'query string absent status' assert resp['headers']['query-string'] == '', 'query string absent' @pytest.mark.skip('not yet') def test_asgi_application_server_port(): client.load('server_port') assert ( client.get()['headers']['Server-Port'] == '8080' ), 'Server-Port header' @pytest.mark.skip('not yet') def test_asgi_application_working_directory_invalid(): client.load('empty') assert 'success' in client.conf( '"/blah"', 'applications/empty/working_directory' ), 'configure invalid working_directory' assert client.get()['status'] == 500, 'status' def test_asgi_application_204_transfer_encoding(): client.load('204_no_content') assert ( 'Transfer-Encoding' not in client.get()['headers'] ), '204 header transfer encoding' def test_asgi_application_shm_ack_handle(): # Minimum possible limit shm_limit = 10 * 1024 * 1024 client.load('mirror', limits={"shm": shm_limit}) # Should exceed shm_limit max_body_size = 12 * 1024 * 1024 assert 'success' in client.conf( f'{{"http":{{"max_body_size": {max_body_size} }}}}', 'settings', ) assert client.get()['status'] == 200, 'init' body = '0123456789AB' * 1024 * 1024 # 12 Mb resp = client.post(body=body, read_buffer_size=1024 * 1024) assert resp['body'] == body, 'keep-alive 1' def test_asgi_keepalive_body(): client.load('mirror') assert client.get()['status'] == 200, 'init' body = '0123456789' * 500 (resp, sock) = client.post( headers={ 'Host': 'localhost', 'Connection': 'keep-alive', }, start=True, body=body, read_timeout=1, ) assert resp['body'] == body, 'keep-alive 1' body = '0123456789' resp = client.post(sock=sock, body=body) assert resp['body'] == body, 'keep-alive 2' def test_asgi_keepalive_reconfigure(): client.load('mirror') assert client.get()['status'] == 200, 'init' body = '0123456789' conns = 3 socks = [] for i in range(conns): (resp, sock) = client.post( headers={ 'Host': 'localhost', 'Connection': 'keep-alive', }, start=True, body=body, read_timeout=1, ) assert resp['body'] == body, 'keep-alive open' client.load('mirror', processes=i + 1) socks.append(sock) for i in range(conns): (resp, sock) = client.post( headers={ 'Host': 'localhost', 'Connection': 'keep-alive', }, start=True, sock=socks[i], body=body, read_timeout=1, ) assert resp['body'] == body, 'keep-alive request' client.load('mirror', processes=i + 1) for i in range(conns): resp = client.post(sock=socks[i], body=body) assert resp['body'] == body, 'keep-alive close' client.load('mirror', processes=i + 1) def test_asgi_keepalive_reconfigure_2(): client.load('mirror') assert client.get()['status'] == 200, 'init' body = '0123456789' (resp, sock) = client.post( headers={ 'Host': 'localhost', 'Connection': 'keep-alive', }, start=True, body=body, read_timeout=1, ) assert resp['body'] == body, 'reconfigure 2 keep-alive 1' client.load('empty') assert client.get()['status'] == 200, 'init' (resp, sock) = client.post(start=True, sock=sock, body=body) assert resp['status'] == 200, 'reconfigure 2 keep-alive 2' assert resp['body'] == '', 'reconfigure 2 keep-alive 2 body' assert 'success' in client.conf( {"listeners": {}, "applications": {}} ), 'reconfigure 2 clear configuration' resp = client.get(sock=sock) assert resp == {}, 'reconfigure 2 keep-alive 3' def test_asgi_keepalive_reconfigure_3(): client.load('empty') assert client.get()['status'] == 200, 'init' sock = client.http( b"""GET / HTTP/1.1 """, raw=True, no_recv=True, ) assert client.get()['status'] == 200 assert 'success' in client.conf( {"listeners": {}, "applications": {}} ), 'reconfigure 3 clear configuration' resp = client.http( b"""Host: localhost Connection: close """, sock=sock, raw=True, ) assert resp['status'] == 200, 'reconfigure 3' def test_asgi_process_switch(): client.load('delayed', processes=2) client.get( headers={ 'Host': 'localhost', 'Content-Length': '0', 'X-Delay': '5', 'Connection': 'close', }, no_recv=True, ) headers_delay_1 = { 'Connection': 'close', 'Host': 'localhost', 'Content-Length': '0', 'X-Delay': '1', } client.get(headers=headers_delay_1, no_recv=True) time.sleep(0.5) for _ in range(10): client.get(headers=headers_delay_1, no_recv=True) client.get(headers=headers_delay_1) def test_asgi_application_loading_error(skip_alert): skip_alert(r'Python failed to import module "blah"') client.load('empty', module="blah") assert client.get()['status'] == 503, 'loading error' def test_asgi_application_threading(wait_for_record): """wait_for_record() timeouts after 5s while every thread works at least 3s. So without releasing GIL test should fail. """ client.load('threading') for _ in range(10): client.get(no_recv=True) assert ( wait_for_record(r'\(5\) Thread: 100', wait=50) is not None ), 'last thread finished' def test_asgi_application_threads(): client.load('threads', threads=2) socks = [] for _ in range(2): sock = client.get( headers={ 'Host': 'localhost', 'X-Delay': '3', 'Connection': 'close', }, no_recv=True, ) socks.append(sock) time.sleep(1.0) # required to avoid greedy request reading threads = set() for sock in socks: resp = client.recvall(sock).decode('utf-8') client.log_in(resp) resp = client._resp_to_dict(resp) assert resp['status'] == 200, 'status' threads.add(resp['headers']['x-thread']) sock.close() assert len(socks) == len(threads), 'threads differs' def test_asgi_application_legacy(): client.load('legacy') resp = client.get( headers={ 'Host': 'localhost', 'Content-Length': '0', 'Connection': 'close', }, ) assert resp['status'] == 200, 'status' def test_asgi_application_legacy_force(): client.load('legacy_force', protocol='asgi') resp = client.get( headers={ 'Host': 'localhost', 'Content-Length': '0', 'Connection': 'close', }, ) assert resp['status'] == 200, 'status'