diff options
author | Konstantin Pavlov <thresh@nginx.com> | 2022-12-15 08:17:39 -0800 |
---|---|---|
committer | Konstantin Pavlov <thresh@nginx.com> | 2022-12-15 08:17:39 -0800 |
commit | e22669f2728814aba82da14702d18bfa9685311e (patch) | |
tree | c9c9471dab359e8e33fca24c5d4f035ab5b278db /test | |
parent | a1d28488f9df8e28ee25ea438c275b96b9afe5b6 (diff) | |
parent | 4409a10ff0bd6bb45fb88716bd383cd867958a8a (diff) | |
download | unit-e22669f2728814aba82da14702d18bfa9685311e.tar.gz unit-e22669f2728814aba82da14702d18bfa9685311e.tar.bz2 |
Merged with the default branch.
Diffstat (limited to 'test')
35 files changed, 1096 insertions, 403 deletions
diff --git a/test/conftest.py b/test/conftest.py index 18851baa..4a1aa7cc 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -17,6 +17,7 @@ import pytest from unit.check.chroot import check_chroot from unit.check.go import check_go from unit.check.isolation import check_isolation +from unit.check.njs import check_njs from unit.check.node import check_node from unit.check.regex import check_regex from unit.check.tls import check_openssl @@ -25,8 +26,10 @@ from unit.http import TestHTTP from unit.log import Log from unit.option import option from unit.status import Status +from unit.utils import check_findmnt from unit.utils import public_dir from unit.utils import waitforfiles +from unit.utils import waitforunmount def pytest_addoption(parser): @@ -86,6 +89,7 @@ _fds_info = { }, } http = TestHTTP() +is_findmnt = check_findmnt() def pytest_configure(config): @@ -176,6 +180,9 @@ def pytest_sessionstart(session): option.available = {'modules': {}, 'features': {}} unit = unit_run() + output_version = subprocess.check_output( + [unit['unitd'], '--version'], stderr=subprocess.STDOUT + ).decode() # read unit.log @@ -202,10 +209,11 @@ def pytest_sessionstart(session): # discover modules from check - option.available['modules']['openssl'] = check_openssl(unit['unitd']) option.available['modules']['go'] = check_go() + option.available['modules']['njs'] = check_njs(output_version) option.available['modules']['node'] = check_node(option.current_dir) - option.available['modules']['regex'] = check_regex(unit['unitd']) + option.available['modules']['openssl'] = check_openssl(output_version) + option.available['modules']['regex'] = check_regex(output_version) # remove None values @@ -310,6 +318,9 @@ def run(request): if not option.restart: _clear_conf(unit['temp_dir'] + '/control.unit.sock', log=log) + if is_findmnt and not waitforunmount(unit['temp_dir'], timeout=600): + exit('Could not unmount some filesystems in tmp dir.') + for item in os.listdir(unit['temp_dir']): if item not in [ 'control.unit.sock', @@ -480,14 +491,15 @@ def _check_alerts(*, log=None): log = f.read() found = False - alerts = re.findall(r'.+\[alert\].+', log) if alerts: - print('\nAll alerts/sanitizer errors found in log:') - [print(alert) for alert in alerts] found = True + if option.detailed: + print('\nAll alerts/sanitizer errors found in log:') + [print(alert) for alert in alerts] + if option.skip_alerts: for skip in option.skip_alerts: alerts = [al for al in alerts if re.search(skip, al) is None] @@ -499,7 +511,7 @@ def _check_alerts(*, log=None): assert not sanitizer_errors, 'sanitizer error(s)' - if found: + if found and option.detailed: print('skipped.') @@ -571,6 +583,10 @@ def _check_processes(): time.sleep(0.1) + if option.restart: + assert len(out) == 0, 'all termimated' + return + assert len(out) == 3, 'main, router, and controller expected' out = [l for l in out if 'unit: main' not in l] diff --git a/test/python/prefix/asgi.py b/test/python/prefix/asgi.py new file mode 100644 index 00000000..234f084f --- /dev/null +++ b/test/python/prefix/asgi.py @@ -0,0 +1,15 @@ +async def application(scope, receive, send): + assert scope['type'] == 'http' + + await send( + { + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + (b'content-length', b'0'), + (b'prefix', scope.get('root_path', 'NULL').encode()), + ], + } + ) + + await send({'type': 'http.response.body', 'body': b''}) diff --git a/test/python/prefix/wsgi.py b/test/python/prefix/wsgi.py new file mode 100644 index 00000000..83b58c9a --- /dev/null +++ b/test/python/prefix/wsgi.py @@ -0,0 +1,10 @@ +def application(environ, start_response): + start_response( + '200', + [ + ('Content-Length', '0'), + ('Script-Name', environ.get('SCRIPT_NAME', 'NULL')), + ('Path-Info', environ['PATH_INFO']), + ], + ) + return [] diff --git a/test/python/targets/asgi.py b/test/python/targets/asgi.py index b51f3964..749ec5b1 100644 --- a/test/python/targets/asgi.py +++ b/test/python/targets/asgi.py @@ -22,6 +22,23 @@ async def application_200(scope, receive, send): ) +async def application_prefix(scope, receive, send): + assert scope['type'] == 'http' + + await send( + { + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + (b'content-length', b'0'), + (b'prefix', scope.get('root_path', 'NULL').encode()), + ], + } + ) + + await send({'type': 'http.response.body', 'body': b''}) + + def legacy_application_200(scope): assert scope['type'] == 'http' diff --git a/test/python/targets/wsgi.py b/test/python/targets/wsgi.py index fa17ab87..3f3d4b27 100644 --- a/test/python/targets/wsgi.py +++ b/test/python/targets/wsgi.py @@ -6,3 +6,12 @@ def wsgi_target_a(env, start_response): def wsgi_target_b(env, start_response): start_response('200', [('Content-Length', '1')]) return [b'2'] + + +def wsgi_target_prefix(env, start_response): + data = u'%s %s' % ( + env.get('SCRIPT_NAME', 'No Script Name'), + env['PATH_INFO'], + ) + start_response('200', [('Content-Length', '%d' % len(data))]) + return [data.encode('utf-8')] diff --git a/test/test_access_log.py b/test/test_access_log.py index b1d89343..a072858b 100644 --- a/test/test_access_log.py +++ b/test/test_access_log.py @@ -280,28 +280,6 @@ Connection: close def test_access_log_variables(self): self.load('mirror') - # $time_local - - self.set_format('$uri $time_local $uri') - assert self.get(url='/time_local')['status'] == 200 - assert self.wait_for_record('/time_local') is not None, 'time log' - date = self.search_in_log( - r'^\/time_local (.*) \/time_local$', 'access.log' - )[1] - assert ( - abs( - self.date_to_sec_epoch(date, '%d/%b/%Y:%X %z') - - time.mktime(time.localtime()) - ) - < 5 - ), '$time_local' - - # $request_line - - self.set_format('$request_line') - assert self.get(url='/r_line')['status'] == 200 - assert self.wait_for_record(r'^GET \/r_line HTTP\/1\.1$') is not None - # $body_bytes_sent self.set_format('$uri $body_bytes_sent') diff --git a/test/test_asgi_application.py b/test/test_asgi_application.py index 34dfe18e..121a2fbc 100644 --- a/test/test_asgi_application.py +++ b/test/test_asgi_application.py @@ -79,6 +79,43 @@ custom-header: BLAH resp['headers']['query-string'] == 'var1=val1&var2=val2' ), 'query-string header' + def test_asgi_application_prefix(self): + self.load('prefix', prefix='/api/rest') + + def set_prefix(prefix): + self.conf('"' + prefix + '"', 'applications/prefix/prefix') + + def check_prefix(url, prefix): + resp = self.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(self): self.load('query_string') @@ -277,10 +314,9 @@ custom-header: BLAH assert self.get()['status'] == 200, 'init' - (_, sock) = self.http( + sock = self.http( b"""GET / HTTP/1.1 """, - start=True, raw=True, no_recv=True, ) @@ -358,14 +394,13 @@ Connection: close socks = [] for i in range(2): - (_, sock) = self.get( + sock = self.get( headers={ 'Host': 'localhost', 'X-Delay': '3', 'Connection': 'close', }, no_recv=True, - start=True, ) socks.append(sock) diff --git a/test/test_asgi_targets.py b/test/test_asgi_targets.py index c1e345ef..84d7b3b0 100644 --- a/test/test_asgi_targets.py +++ b/test/test_asgi_targets.py @@ -90,3 +90,48 @@ class TestASGITargets(TestApplicationPython): ) assert self.get(url='/1')['status'] != 200 + + def test_asgi_targets_prefix(self): + self.conf_targets( + { + "1": { + "module": "asgi", + "callable": "application_prefix", + "prefix": "/1/", + }, + "2": { + "module": "asgi", + "callable": "application_prefix", + "prefix": "/api", + }, + } + ) + self.conf( + [ + { + "match": {"uri": "/1*"}, + "action": {"pass": "applications/targets/1"}, + }, + { + "match": {"uri": "*"}, + "action": {"pass": "applications/targets/2"}, + }, + ], + "routes", + ) + + def check_prefix(url, prefix): + resp = self.get(url=url) + assert resp['status'] == 200 + assert resp['headers']['prefix'] == prefix + + check_prefix('/1', '/1') + check_prefix('/11', 'NULL') + check_prefix('/1/', '/1') + check_prefix('/', 'NULL') + check_prefix('/ap', 'NULL') + check_prefix('/api', '/api') + check_prefix('/api/', '/api') + check_prefix('/api/test/', '/api') + check_prefix('/apis', 'NULL') + check_prefix('/apis/', 'NULL') diff --git a/test/test_configuration.py b/test/test_configuration.py index 7c612db0..9c27222c 100644 --- a/test/test_configuration.py +++ b/test/test_configuration.py @@ -318,6 +318,92 @@ class TestConfiguration(TestControl): assert 'success' in self.conf(conf) + def test_json_application_python_prefix(self): + conf = { + "applications": { + "sub-app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + "prefix": "/app", + } + }, + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [ + { + "match": {"uri": "/app/*"}, + "action": {"pass": "applications/sub-app"}, + } + ], + } + + assert 'success' in self.conf(conf) + + def test_json_application_prefix_target(self): + conf = { + "applications": { + "sub-app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "targets": { + "foo": {"module": "foo.wsgi", "prefix": "/app"}, + "bar": { + "module": "bar.wsgi", + "callable": "bar", + "prefix": "/api", + }, + }, + } + }, + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [ + { + "match": {"uri": "/app/*"}, + "action": {"pass": "applications/sub-app/foo"}, + }, + { + "match": {"uri": "/api/*"}, + "action": {"pass": "applications/sub-app/bar"}, + }, + ], + } + + assert 'success' in self.conf(conf) + + def test_json_application_invalid_python_prefix(self): + conf = { + "applications": { + "sub-app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + "prefix": "app", + } + }, + "listeners": {"*:7080": {"pass": "applications/sub-app"}}, + } + + assert 'error' in self.conf(conf) + + def test_json_application_empty_python_prefix(self): + conf = { + "applications": { + "sub-app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + "prefix": "", + } + }, + "listeners": {"*:7080": {"pass": "applications/sub-app"}}, + } + + assert 'error' in self.conf(conf) + def test_json_application_many2(self): conf = { "applications": { diff --git a/test/test_java_application.py b/test/test_java_application.py index adcb4eca..b825d925 100644 --- a/test/test_java_application.py +++ b/test/test_java_application.py @@ -71,6 +71,11 @@ class TestJavaApplication(TestApplicationJava): def test_java_application_get_variables(self): self.load('get_params') + def check_header(header, expect): + values = header.split(' ')[:-1] + assert len(values) == len(expect) + assert set(values) == set(expect) + headers = self.get(url='/?var1=val1&var2=&var4=val4&var4=foo')[ 'headers' ] @@ -79,13 +84,11 @@ class TestJavaApplication(TestApplicationJava): assert headers['X-Var-2'] == 'true', 'GET variables 2' assert headers['X-Var-3'] == 'false', 'GET variables 3' - assert ( - headers['X-Param-Names'] == 'var4 var2 var1 ' - ), 'getParameterNames' - assert headers['X-Param-Values'] == 'val4 foo ', 'getParameterValues' - assert ( - headers['X-Param-Map'] == 'var2= var1=val1 var4=val4,foo ' - ), 'getParameterMap' + check_header(headers['X-Param-Names'], ['var4', 'var2', 'var1']) + check_header(headers['X-Param-Values'], ['val4', 'foo']) + check_header( + headers['X-Param-Map'], ['var2=', 'var1=val1', 'var4=val4,foo'] + ) def test_java_application_post_variables(self): self.load('post_params') @@ -1001,14 +1004,13 @@ class TestJavaApplication(TestApplicationJava): socks = [] for i in range(4): - (_, sock) = self.get( + sock = self.get( headers={ 'Host': 'localhost', 'X-Delay': '2', 'Connection': 'close', }, no_recv=True, - start=True, ) socks.append(sock) diff --git a/test/test_njs.py b/test/test_njs.py new file mode 100644 index 00000000..2cbded5b --- /dev/null +++ b/test/test_njs.py @@ -0,0 +1,90 @@ +import os + +from unit.applications.proto import TestApplicationProto +from unit.option import option + + +class TestNJS(TestApplicationProto): + prerequisites = {'modules': {'njs': 'any'}} + + def setup_method(self): + os.makedirs(option.temp_dir + '/assets') + open(option.temp_dir + '/assets/index.html', 'a') + open(option.temp_dir + '/assets/localhost', 'a') + open(option.temp_dir + '/assets/`string`', 'a') + open(option.temp_dir + '/assets/`backtick', 'a') + open(option.temp_dir + '/assets/l1\nl2', 'a') + open(option.temp_dir + '/assets/127.0.0.1', 'a') + + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [ + {"action": {"share": option.temp_dir + "/assets$uri"}} + ], + } + ) + + def set_share(self, share): + assert 'success' in self.conf(share, 'routes/0/action/share') + + def test_njs_template_string(self, temp_dir): + self.set_share('"`' + temp_dir + '/assets/index.html`"') + assert self.get()['status'] == 200, 'string' + + self.set_share('"' + temp_dir + '/assets/`string`"') + assert self.get()['status'] == 200, 'string 2' + + self.set_share('"`' + temp_dir + '/assets/\\\\`backtick`"') + assert self.get()['status'] == 200, 'escape' + + self.set_share('"`' + temp_dir + '/assets/l1\\nl2`"') + assert self.get()['status'] == 200, 'multiline' + + def test_njs_template_expression(self, temp_dir): + def check_expression(expression): + self.set_share(expression) + assert self.get()['status'] == 200 + + check_expression('"`' + temp_dir + '/assets${uri}`"') + check_expression('"`' + temp_dir + '/assets${uri}${host}`"') + check_expression('"`' + temp_dir + '/assets${uri + host}`"') + check_expression('"`' + temp_dir + '/assets${uri + `${host}`}`"') + + def test_njs_variables(self, temp_dir): + self.set_share('"`' + temp_dir + '/assets/${host}`"') + assert self.get()['status'] == 200, 'host' + + self.set_share('"`' + temp_dir + '/assets/${remoteAddr}`"') + assert self.get()['status'] == 200, 'remoteAddr' + + self.set_share('"`' + temp_dir + '/assets/${headers.Host}`"') + assert self.get()['status'] == 200, 'headers' + + self.set_share('"`' + temp_dir + '/assets/${cookies.foo}`"') + assert ( + self.get( + headers={'Cookie': 'foo=localhost', 'Connection': 'close'} + )['status'] + == 200 + ), 'cookies' + + self.set_share('"`' + temp_dir + '/assets/${args.foo}`"') + assert self.get(url='/?foo=localhost')['status'] == 200, 'args' + + def test_njs_invalid(self, temp_dir, skip_alert): + skip_alert(r'js exception:') + + def check_invalid(template): + assert 'error' in self.conf(template, 'routes/0/action/share') + + check_invalid('"`a"') + check_invalid('"`a``"') + check_invalid('"`a`/"') + + def check_invalid_resolve(template): + assert 'success' in self.conf(template, 'routes/0/action/share') + assert self.get()['status'] == 500 + + check_invalid_resolve('"`${a}`"') + check_invalid_resolve('"`${uri.a.a}`"') diff --git a/test/test_perl_application.py b/test/test_perl_application.py index 0d1d7906..fe2db72e 100644 --- a/test/test_perl_application.py +++ b/test/test_perl_application.py @@ -259,14 +259,13 @@ class TestPerlApplication(TestApplicationPerl): socks = [] for i in range(4): - (_, sock) = self.get( + sock = self.get( headers={ 'Host': 'localhost', 'X-Delay': '2', 'Connection': 'close', }, no_recv=True, - start=True, ) socks.append(sock) diff --git a/test/test_php_application.py b/test/test_php_application.py index f1dcc995..f442f551 100644 --- a/test/test_php_application.py +++ b/test/test_php_application.py @@ -4,6 +4,7 @@ import re import shutil import signal import time +from pathlib import Path import pytest from unit.applications.lang.php import TestApplicationPHP @@ -620,6 +621,49 @@ opcache.preload_user = %(user)s assert resp['status'] == 200, 'status' assert resp['body'] != '', 'body not empty' + def test_php_application_trailing_slash(self, temp_dir): + new_root = temp_dir + "/php-root" + os.makedirs(new_root + '/path') + + Path(new_root + '/path/index.php').write_text('<?php echo "OK\n"; ?>') + + addr = temp_dir + '/sock' + + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "applications/php-path"}, + "unix:" + addr: {"pass": "applications/php-path"}, + }, + "applications": { + "php-path": { + "type": self.get_application_type(), + "processes": {"spare": 0}, + "root": new_root, + } + }, + } + ), 'configure trailing slash' + + assert self.get(url='/path/')['status'] == 200, 'uri with trailing /' + + resp = self.get(url='/path?q=a') + assert resp['status'] == 301, 'uri without trailing /' + assert ( + resp['headers']['Location'] == 'http://localhost:7080/path/?q=a' + ), 'Location with query string' + + resp = self.get( + sock_type='unix', + addr=addr, + url='/path', + headers={'Host': 'foo', 'Connection': 'close'}, + ) + assert resp['status'] == 301, 'uri without trailing /' + assert ( + resp['headers']['Location'] == 'http://foo/path/' + ), 'Location with custom Host over UDS' + def test_php_application_extension_check(self, temp_dir): self.load('phpinfo') diff --git a/test/test_proxy.py b/test/test_proxy.py index b0d471e4..ede91fd6 100644 --- a/test/test_proxy.py +++ b/test/test_proxy.py @@ -185,9 +185,8 @@ Content-Length: 10 socks = [] for i in range(10): - _, sock = self.post_http10( + sock = self.post_http10( body=payload + str(i), - start=True, no_recv=True, read_buffer_size=buff_size, ) @@ -248,9 +247,7 @@ Content-Length: 10 ), 'custom header 5' def test_proxy_fragmented(self): - _, sock = self.http( - b"""GET / HTT""", raw=True, start=True, no_recv=True - ) + sock = self.http(b"""GET / HTT""", raw=True, no_recv=True) time.sleep(1) @@ -266,9 +263,7 @@ Content-Length: 10 sock.close() def test_proxy_fragmented_close(self): - _, sock = self.http( - b"""GET / HTT""", raw=True, start=True, no_recv=True - ) + sock = self.http(b"""GET / HTT""", raw=True, no_recv=True) time.sleep(1) @@ -277,9 +272,7 @@ Content-Length: 10 sock.close() def test_proxy_fragmented_body(self): - _, sock = self.http( - b"""GET / HTT""", raw=True, start=True, no_recv=True - ) + sock = self.http(b"""GET / HTT""", raw=True, no_recv=True) time.sleep(1) @@ -306,9 +299,7 @@ Content-Length: 10 assert resp['body'] == "X" * 30000, 'body' def test_proxy_fragmented_body_close(self): - _, sock = self.http( - b"""GET / HTT""", raw=True, start=True, no_recv=True - ) + sock = self.http(b"""GET / HTT""", raw=True, no_recv=True) time.sleep(1) @@ -398,7 +389,7 @@ Content-Length: 10 {"pass": "applications/delayed"}, 'listeners/*:7081' ), 'delayed configure' - _, sock = self.post_http10( + sock = self.post_http10( headers={ 'Host': 'localhost', 'Content-Length': '10000', @@ -406,14 +397,13 @@ Content-Length: 10 'X-Delay': '1', }, body='0123456789' * 1000, - start=True, no_recv=True, ) assert re.search('200 OK', sock.recv(100).decode()), 'first' sock.close() - _, sock = self.post_http10( + sock = self.post_http10( headers={ 'Host': 'localhost', 'Content-Length': '10000', @@ -421,7 +411,6 @@ Content-Length: 10 'X-Delay': '1', }, body='0123456789' * 1000, - start=True, no_recv=True, ) diff --git a/test/test_python_application.py b/test/test_python_application.py index 2ea9a22e..c9483b6a 100644 --- a/test/test_python_application.py +++ b/test/test_python_application.py @@ -94,6 +94,44 @@ custom-header: BLAH resp['headers']['Query-String'] == ' var1= val1 & var2=val2' ), 'Query-String space 4' + def test_python_application_prefix(self): + self.load('prefix', prefix='/api/rest') + + def set_prefix(prefix): + self.conf('"' + prefix + '"', 'applications/prefix/prefix') + + def check_prefix(url, script_name, path_info): + resp = self.get(url=url) + assert resp['status'] == 200 + assert resp['headers']['Script-Name'] == script_name + assert resp['headers']['Path-Info'] == path_info + + check_prefix('/ap', 'NULL', '/ap') + check_prefix('/api', 'NULL', '/api') + check_prefix('/api/', 'NULL', '/api/') + check_prefix('/api/res', 'NULL', '/api/res') + check_prefix('/api/restful', 'NULL', '/api/restful') + check_prefix('/api/rest', '/api/rest', '') + check_prefix('/api/rest/', '/api/rest', '/') + check_prefix('/api/rest/get', '/api/rest', '/get') + check_prefix('/api/rest/get/blah', '/api/rest', '/get/blah') + + set_prefix('/api/rest/') + check_prefix('/api/rest', '/api/rest', '') + check_prefix('/api/restful', 'NULL', '/api/restful') + check_prefix('/api/rest/', '/api/rest', '/') + check_prefix('/api/rest/blah', '/api/rest', '/blah') + + set_prefix('/app') + check_prefix('/ap', 'NULL', '/ap') + check_prefix('/app', '/app', '') + check_prefix('/app/', '/app', '/') + check_prefix('/application/', 'NULL', '/application/') + + set_prefix('/') + check_prefix('/', 'NULL', '/') + check_prefix('/app', 'NULL', '/app') + def test_python_application_query_string_empty(self): self.load('query_string') @@ -765,14 +803,13 @@ last line: 987654321 socks = [] for i in range(4): - (_, sock) = self.get( + sock = self.get( headers={ 'Host': 'localhost', 'X-Delay': '2', 'Connection': 'close', }, no_recv=True, - start=True, ) socks.append(sock) diff --git a/test/test_python_isolation.py b/test/test_python_isolation.py index 8cef6812..6d4ffaf3 100644 --- a/test/test_python_isolation.py +++ b/test/test_python_isolation.py @@ -1,3 +1,8 @@ +import os +import re +import subprocess +from pathlib import Path + import pytest from unit.applications.lang.python import TestApplicationPython from unit.option import option @@ -9,6 +14,23 @@ from unit.utils import waitforunmount class TestPythonIsolation(TestApplicationPython): prerequisites = {'modules': {'python': 'any'}, 'features': ['isolation']} + def get_cgroup(self, app_name): + output = subprocess.check_output( + ['ps', 'ax', '-o', 'pid', '-o', 'cmd'] + ).decode() + + pid = re.search( + r'(\d+)\s*unit: "' + app_name + '" application', output + ).group(1) + + cgroup = '/proc/' + pid + '/cgroup' + + if not os.path.isfile(cgroup): + pytest.skip('no cgroup at ' + cgroup) + + with open(cgroup, 'r') as f: + return f.read().rstrip() + def test_python_isolation_rootfs(self, is_su, temp_dir): isolation_features = option.available['features']['isolation'].keys() @@ -63,24 +85,25 @@ class TestPythonIsolation(TestApplicationPython): pytest.skip('requires root') isolation = {'rootfs': temp_dir, 'automount': {'language_deps': False}} - self.load('empty', isolation=isolation) - assert findmnt().find(temp_dir) == -1 + python_path = temp_dir + '/usr' + + assert findmnt().find(python_path) == -1 assert self.get()['status'] != 200, 'disabled language_deps' - assert findmnt().find(temp_dir) == -1 + assert findmnt().find(python_path) == -1 isolation['automount']['language_deps'] = True self.load('empty', isolation=isolation) - assert findmnt().find(temp_dir) == -1 + assert findmnt().find(python_path) == -1 assert self.get()['status'] == 200, 'enabled language_deps' - assert waitformount(temp_dir), 'language_deps mount' + assert waitformount(python_path), 'language_deps mount' self.conf({"listeners": {}, "applications": {}}) - assert waitforunmount(temp_dir), 'language_deps unmount' + assert waitforunmount(python_path), 'language_deps unmount' def test_python_isolation_procfs(self, is_su, temp_dir): if not is_su: @@ -101,3 +124,104 @@ class TestPythonIsolation(TestApplicationPython): assert ( self.getjson(url='/?path=/proc/self')['body']['FileExists'] == True ), '/proc/self' + + def test_python_isolation_cgroup(self, is_su, temp_dir): + if not is_su: + pytest.skip('requires root') + + if not 'cgroup' in option.available['features']['isolation']: + pytest.skip('cgroup is not supported') + + def set_cgroup_path(path): + isolation = {'cgroup': {'path': path}} + self.load('empty', processes=1, isolation=isolation) + + set_cgroup_path('scope/python') + + cgroup_rel = Path(self.get_cgroup('empty')) + assert cgroup_rel.parts[-2:] == ('scope', 'python'), 'cgroup rel' + + set_cgroup_path('/scope2/python') + + cgroup_abs = Path(self.get_cgroup('empty')) + assert cgroup_abs.parts[-2:] == ('scope2', 'python'), 'cgroup abs' + + assert len(cgroup_rel.parts) >= len(cgroup_abs.parts) + + def test_python_isolation_cgroup_two(self, is_su, temp_dir): + if not is_su: + pytest.skip('requires root') + + if not 'cgroup' in option.available['features']['isolation']: + pytest.skip('cgroup is not supported') + + def set_two_cgroup_path(path, path2): + script_path = option.test_dir + '/python/empty' + + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "applications/one"}, + "*:7081": {"pass": "applications/two"}, + }, + "applications": { + "one": { + "type": "python", + "processes": 1, + "path": script_path, + "working_directory": script_path, + "module": "wsgi", + "isolation": { + 'cgroup': {'path': path}, + }, + }, + "two": { + "type": "python", + "processes": 1, + "path": script_path, + "working_directory": script_path, + "module": "wsgi", + "isolation": { + 'cgroup': {'path': path2}, + }, + }, + }, + } + ) + + set_two_cgroup_path('/scope/python', '/scope/python') + assert self.get_cgroup('one') == self.get_cgroup('two') + + set_two_cgroup_path('/scope/python', '/scope2/python') + assert self.get_cgroup('one') != self.get_cgroup('two') + + def test_python_isolation_cgroup_invalid(self, is_su): + if not is_su: + pytest.skip('requires root') + + if not 'cgroup' in option.available['features']['isolation']: + pytest.skip('cgroup is not supported') + + def check_invalid(path): + script_path = option.test_dir + '/python/empty' + assert 'error' in self.conf( + { + "listeners": {"*:7080": {"pass": "applications/empty"}}, + "applications": { + "empty": { + "type": "python", + "processes": {"spare": 0}, + "path": script_path, + "working_directory": script_path, + "module": "wsgi", + "isolation": { + 'cgroup': {'path': path}, + }, + } + }, + } + ) + + check_invalid('') + check_invalid('../scope') + check_invalid('scope/../python') diff --git a/test/test_python_targets.py b/test/test_python_targets.py index 8e9ecb87..ae271b5f 100644 --- a/test/test_python_targets.py +++ b/test/test_python_targets.py @@ -47,3 +47,55 @@ class TestPythonTargets(TestApplicationPython): resp = self.get(url='/2') assert resp['status'] == 200 assert resp['body'] == '2' + + def test_python_targets_prefix(self): + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [ + { + "match": {"uri": ["/app*"]}, + "action": {"pass": "applications/targets/app"}, + }, + { + "match": {"uri": "*"}, + "action": {"pass": "applications/targets/catchall"}, + }, + ], + "applications": { + "targets": { + "type": "python", + "working_directory": option.test_dir + + "/python/targets/", + "path": option.test_dir + '/python/targets/', + "protocol": "wsgi", + "targets": { + "app": { + "module": "wsgi", + "callable": "wsgi_target_prefix", + "prefix": "/app/", + }, + "catchall": { + "module": "wsgi", + "callable": "wsgi_target_prefix", + "prefix": "/api", + }, + }, + } + }, + } + ) + + def check_prefix(url, body): + resp = self.get(url=url) + assert resp['status'] == 200 + assert resp['body'] == body + + check_prefix('/app', '/app ') + check_prefix('/app/', '/app /') + check_prefix('/app/rest/user/', '/app /rest/user/') + check_prefix('/catchall', 'No Script Name /catchall') + check_prefix('/api', '/api ') + check_prefix('/api/', '/api /') + check_prefix('/apis', 'No Script Name /apis') + check_prefix('/api/users/', '/api /users/') diff --git a/test/test_reconfigure.py b/test/test_reconfigure.py index ab05a1c8..feb027aa 100644 --- a/test/test_reconfigure.py +++ b/test/test_reconfigure.py @@ -21,10 +21,9 @@ class TestReconfigure(TestApplicationProto): assert 'success' in self.conf({"listeners": {}, "applications": {}}) def test_reconfigure(self): - (_, sock) = self.http( + sock = self.http( b"""GET / HTTP/1.1 """, - start=True, raw=True, no_recv=True, ) @@ -42,7 +41,7 @@ Connection: close assert resp['status'] == 200, 'finish request' def test_reconfigure_2(self): - (_, sock) = self.http(b'', raw=True, start=True, no_recv=True) + sock = self.http(b'', raw=True, no_recv=True) # Waiting for connection completion. # Delay should be more than TCP_DEFER_ACCEPT. diff --git a/test/test_routing.py b/test/test_routing.py index 3649b37c..9e872061 100644 --- a/test/test_routing.py +++ b/test/test_routing.py @@ -1401,6 +1401,20 @@ class TestRouting(TestApplicationPython): self.route_match_invalid({"cookies": ["var"]}) self.route_match_invalid({"cookies": [{"foo": {}}]}) + def test_routes_match_cookies_complex(self): + self.route_match({"cookies": {"foo": "bar=baz"}}) + self.cookie('foo=bar=baz', 200) + self.cookie(' foo=bar=baz ', 200) + self.cookie('=foo=bar=baz', 404) + + self.route_match({"cookies": {"foo": ""}}) + self.cookie('foo=', 200) + self.cookie('foo=;', 200) + self.cookie(' foo=;', 200) + self.cookie('foo', 404) + self.cookie('', 404) + self.cookie('=', 404) + def test_routes_match_cookies_multiple(self): self.route_match({"cookies": {"foo": "bar", "blah": "blah"}}) @@ -1471,7 +1485,7 @@ class TestRouting(TestApplicationPython): def test_routes_source_port(self): def sock_port(): - _, sock = self.http(b'', start=True, raw=True, no_recv=True) + sock = self.http(b'', raw=True, no_recv=True) port = sock.getsockname()[1] return (sock, port) diff --git a/test/test_ruby_application.py b/test/test_ruby_application.py index 83af39be..068b587b 100644 --- a/test/test_ruby_application.py +++ b/test/test_ruby_application.py @@ -388,14 +388,13 @@ class TestRubyApplication(TestApplicationRuby): socks = [] for i in range(4): - (_, sock) = self.get( + sock = self.get( headers={ 'Host': 'localhost', 'X-Delay': '2', 'Connection': 'close', }, no_recv=True, - start=True, ) socks.append(sock) diff --git a/test/test_settings.py b/test/test_settings.py index ea3cfb99..ad8929f8 100644 --- a/test/test_settings.py +++ b/test/test_settings.py @@ -10,6 +10,66 @@ from unit.utils import sysctl class TestSettings(TestApplicationPython): prerequisites = {'modules': {'python': 'any'}} + def test_settings_large_header_buffer_size(self): + self.load('empty') + + def set_buffer_size(size): + assert 'success' in self.conf( + {'http': {'large_header_buffer_size': size}}, + 'settings', + ) + + def header_value(size, expect=200): + headers = {'Host': 'a' * (size - 1), 'Connection': 'close'} + assert self.get(headers=headers)['status'] == expect + + set_buffer_size(4096) + header_value(4096) + header_value(4097, 431) + + set_buffer_size(16384) + header_value(16384) + header_value(16385, 431) + + def test_settings_large_header_buffers(self): + self.load('empty') + + def set_buffers(buffers): + assert 'success' in self.conf( + {'http': {'large_header_buffers': buffers}}, + 'settings', + ) + + def big_headers(headers_num, expect=200): + headers = {'Host': 'localhost', 'Connection': 'close'} + + for i in range(headers_num): + headers['Custom-header-' + str(i)] = 'a' * 8000 + + assert self.get(headers=headers)['status'] == expect + + set_buffers(1) + big_headers(1) + big_headers(2, 431) + + set_buffers(2) + big_headers(2) + big_headers(3, 431) + + set_buffers(8) + big_headers(8) + big_headers(9, 431) + + @pytest.mark.skip('not yet') + def test_settings_large_header_buffer_invalid(self): + def check_error(conf): + assert 'error' in self.conf({'http': conf}, 'settings') + + check_error({'large_header_buffer_size': -1}) + check_error({'large_header_buffer_size': 0}) + check_error({'large_header_buffers': -1}) + check_error({'large_header_buffers': 0}) + def test_settings_header_read_timeout(self): self.load('empty') @@ -50,20 +110,18 @@ Connection: close {'http': {'header_read_timeout': 4}}, 'settings' ) - (resp, sock) = self.http( + sock = self.http( b"""GET / HTTP/1.1 """, - start=True, raw=True, no_recv=True, ) time.sleep(2) - (resp, sock) = self.http( + sock = self.http( b"""Host: localhost """, - start=True, sock=sock, raw=True, no_recv=True, @@ -245,7 +303,7 @@ Connection: close self.load('empty') def req(): - _, sock = self.http(b'', start=True, raw=True, no_recv=True) + sock = self.http(b'', raw=True, no_recv=True) time.sleep(3) diff --git a/test/test_static.py b/test/test_static.py index b9c78fdd..9013b5c0 100644 --- a/test/test_static.py +++ b/test/test_static.py @@ -1,10 +1,7 @@ import os -import shutil import socket import pytest -from conftest import unit_run -from conftest import unit_stop from unit.applications.proto import TestApplicationProto from unit.option import option from unit.utils import waitforfiles @@ -43,49 +40,6 @@ class TestStatic(TestApplicationProto): } ) - def test_static_migration(self, skip_fds_check, temp_dir): - skip_fds_check(True, True, True) - - def set_conf_version(path, version): - with open(path, 'w+') as f: - f.write(str(version)) - - with open(temp_dir + '/state/version', 'r') as f: - assert int(f.read().rstrip()) > 12500, 'current version' - - assert 'success' in self.conf( - {"share": temp_dir + "/assets"}, 'routes/0/action' - ), 'configure migration 12500' - - shutil.copytree(temp_dir + '/state', temp_dir + '/state_copy_12500') - set_conf_version(temp_dir + '/state_copy_12500/version', 12500) - - assert 'success' in self.conf( - {"share": temp_dir + "/assets$uri"}, 'routes/0/action' - ), 'configure migration 12600' - shutil.copytree(temp_dir + '/state', temp_dir + '/state_copy_12600') - set_conf_version(temp_dir + '/state_copy_12600/version', 12600) - - assert 'success' in self.conf( - {"share": temp_dir + "/assets"}, 'routes/0/action' - ), 'configure migration no version' - shutil.copytree( - temp_dir + '/state', temp_dir + '/state_copy_no_version' - ) - os.remove(temp_dir + '/state_copy_no_version/version') - - unit_stop() - unit_run(temp_dir + '/state_copy_12500') - assert self.get(url='/')['body'] == '0123456789', 'before 1.26.0' - - unit_stop() - unit_run(temp_dir + '/state_copy_12600') - assert self.get(url='/')['body'] == '0123456789', 'after 1.26.0' - - unit_stop() - unit_run(temp_dir + '/state_copy_no_version') - assert self.get(url='/')['body'] == '0123456789', 'before 1.26.0 2' - def test_static_index(self): def set_index(index): assert 'success' in self.conf( @@ -262,8 +216,8 @@ class TestStatic(TestApplicationProto): assert self.get(url='/../assets/')['status'] == 400, 'path invalid 5' def test_static_two_clients(self): - _, sock = self.get(url='/', start=True, no_recv=True) - _, sock2 = self.get(url='/', start=True, no_recv=True) + sock = self.get(no_recv=True) + sock2 = self.get(no_recv=True) assert sock.recv(1) == b'H', 'client 1' assert sock2.recv(1) == b'H', 'client 2' diff --git a/test/test_status.py b/test/test_status.py index 214072d4..6c733474 100644 --- a/test/test_status.py +++ b/test/test_status.py @@ -9,6 +9,23 @@ from unit.status import Status class TestStatus(TestApplicationPython): prerequisites = {'modules': {'python': 'any'}} + def check_connections(self, accepted, active, idle, closed): + Status.get('/connections') == { + 'accepted': accepted, + 'active': active, + 'idle': idle, + 'closed': closed, + } + + def app_default(self, name="empty", module="wsgi"): + return { + "type": self.get_application_type(), + "processes": {"spare": 0}, + "path": option.test_dir + "/python/" + name, + "working_directory": option.test_dir + "/python/" + name, + "module": module, + } + def test_status(self): assert 'error' in self.conf_delete('/status'), 'DELETE method' @@ -24,13 +41,7 @@ class TestStatus(TestApplicationPython): }, "routes": [{"action": {"return": 200}}], "applications": { - "empty": { - "type": self.get_application_type(), - "processes": {"spare": 0}, - "path": option.test_dir + '/python/empty', - "working_directory": option.test_dir + '/python/empty', - "module": "wsgi", - }, + "empty": self.app_default(), "blah": { "type": self.get_application_type(), "processes": {"spare": 0}, @@ -70,7 +81,7 @@ Connection: close ) assert Status.get('/requests/total') == 6, 'pipeline' - (_, sock) = self.get(port=7081, no_recv=True, start=True) + sock = self.get(port=7081, no_recv=True) time.sleep(1) @@ -79,14 +90,6 @@ Connection: close sock.close() def test_status_connections(self): - def check_connections(accepted, active, idle, closed): - Status.get('/connections') == { - 'accepted': accepted, - 'active': active, - 'idle': idle, - 'closed': closed, - } - assert 'success' in self.conf( { "listeners": { @@ -95,14 +98,7 @@ Connection: close }, "routes": [{"action": {"return": 200}}], "applications": { - "delayed": { - "type": self.get_application_type(), - "processes": {"spare": 0}, - "path": option.test_dir + "/python/delayed", - "working_directory": option.test_dir - + "/python/delayed", - "module": "wsgi", - }, + "delayed": self.app_default("delayed"), }, }, ) @@ -112,15 +108,15 @@ Connection: close # accepted, closed assert self.get()['status'] == 200 - check_connections(1, 0, 0, 1) + self.check_connections(1, 0, 0, 1) # idle - _, sock = self.http(b'', start=True, raw=True, no_recv=True) - check_connections(2, 0, 1, 1) + sock = self.http(b'', raw=True, no_recv=True) + self.check_connections(2, 0, 1, 1) self.get(sock=sock) - check_connections(2, 0, 0, 2) + self.check_connections(2, 0, 0, 2) # active @@ -134,10 +130,10 @@ Connection: close start=True, read_timeout=1, ) - check_connections(3, 1, 0, 2) + self.check_connections(3, 1, 0, 2) self.get(sock=sock) - check_connections(3, 0, 0, 3) + self.check_connections(3, 0, 0, 3) def test_status_applications(self): def check_applications(expert): @@ -192,22 +188,8 @@ Connection: close }, "routes": [], "applications": { - "restart": { - "type": self.get_application_type(), - "processes": {"spare": 0}, - "path": option.test_dir + "/python/restart", - "working_directory": option.test_dir - + "/python/restart", - "module": "longstart", - }, - "delayed": { - "type": self.get_application_type(), - "processes": {"spare": 0}, - "path": option.test_dir + "/python/delayed", - "working_directory": option.test_dir - + "/python/delayed", - "module": "wsgi", - }, + "restart": self.app_default("restart", "longstart"), + "delayed": self.app_default("delayed"), }, }, ) @@ -221,3 +203,28 @@ Connection: close check_application('restart', 0, 1, 0, 1) check_application('delayed', 0, 0, 0, 0) + + def test_status_proxy(self): + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "routes"}, + "*:7081": {"pass": "applications/empty"}, + }, + "routes": [ + { + "match": {"uri": "/"}, + "action": {"proxy": "http://127.0.0.1:7081"}, + } + ], + "applications": { + "empty": self.app_default(), + }, + }, + ) + + Status.init() + + assert self.get()['status'] == 200 + self.check_connections(2, 0, 0, 2) + assert Status.get('/requests/total') == 2, 'proxy' diff --git a/test/test_upstreams_rr.py b/test/test_upstreams_rr.py index dd64e1d9..71af3f5d 100644 --- a/test/test_upstreams_rr.py +++ b/test/test_upstreams_rr.py @@ -290,14 +290,13 @@ Connection: close socks = [] for i in range(req): delay = 1 if i % 5 == 0 else 0 - _, sock = self.get( + sock = self.get( headers={ 'Host': 'localhost', 'Content-Length': '0', 'X-Delay': str(delay), 'Connection': 'close', }, - start=True, no_recv=True, ) socks.append(sock) @@ -320,17 +319,16 @@ Connection: close socks2 = [] for _ in range(conns): - _, sock = self.get(start=True, no_recv=True) + sock = self.get(no_recv=True) socks.append(sock) - _, sock2 = self.http( + sock2 = self.http( b"""POST / HTTP/1.1 Host: localhost Content-Length: 10 Connection: close """, - start=True, no_recv=True, raw=True, ) diff --git a/test/test_variables.py b/test/test_variables.py index 2ddfdc0a..ecce5e6d 100644 --- a/test/test_variables.py +++ b/test/test_variables.py @@ -1,4 +1,8 @@ +import re +import time + from unit.applications.proto import TestApplicationProto +from unit.option import option class TestVariables(TestApplicationProto): @@ -7,79 +11,194 @@ class TestVariables(TestApplicationProto): def setup_method(self): assert 'success' in self.conf( { - "listeners": {"*:7080": {"pass": "routes/$method"}}, - "routes": { - "GET": [{"action": {"return": 201}}], - "POST": [{"action": {"return": 202}}], - "3": [{"action": {"return": 203}}], - "4*": [{"action": {"return": 204}}], - "blahGET}": [{"action": {"return": 205}}], - "5GET": [{"action": {"return": 206}}], - "GETGET": [{"action": {"return": 207}}], - "localhost": [{"action": {"return": 208}}], - "9?q#a": [{"action": {"return": 209}}], - "blah": [{"action": {"return": 210}}], - "127.0.0.1": [{"action": {"return": 211}}], - "::1": [{"action": {"return": 212}}], - "referer-value": [{"action": {"return": 213}}], - "MSIE": [{"action": {"return": 214}}], - }, + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [{"action": {"return": 200}}], }, ), 'configure routes' - def conf_routes(self, routes): - assert 'success' in self.conf(routes, 'listeners/*:7080/pass') + def set_format(self, format): + assert 'success' in self.conf( + { + 'path': option.temp_dir + '/access.log', + 'format': format, + }, + 'access_log', + ), 'access_log format' + + def wait_for_record(self, pattern, name='access.log'): + return super().wait_for_record(pattern, name) + + def search_in_log(self, pattern, name='access.log'): + return super().search_in_log(pattern, name) + + def test_variables_dollar(self): + assert 'success' in self.conf("301", 'routes/0/action/return') + + def check_dollar(location, expect): + assert 'success' in self.conf( + '"' + location + '"', + 'routes/0/action/location', + ) + assert self.get()['headers']['Location'] == expect + + check_dollar( + 'https://${host}${uri}path${dollar}dollar', + 'https://localhost/path$dollar', + ) + check_dollar('path$dollar${dollar}', 'path$$') + + def test_variables_request_time(self): + self.set_format('$uri $request_time') + + sock = self.http(b'', raw=True, no_recv=True) + + time.sleep(1) + + assert self.get(url='/r_time_1', sock=sock)['status'] == 200 + assert self.wait_for_record(r'\/r_time_1 0\.\d{3}') is not None + + sock = self.http( + b"""G""", + no_recv=True, + raw=True, + ) + + time.sleep(2) + + self.http( + b"""ET /r_time_2 HTTP/1.1 +Host: localhost +Connection: close + +""", + sock=sock, + raw=True, + ) + assert self.wait_for_record(r'\/r_time_2 [1-9]\.\d{3}') is not None def test_variables_method(self): - assert self.get()['status'] == 201, 'method GET' - assert self.post()['status'] == 202, 'method POST' + self.set_format('$method') + + reg = r'^GET$' + assert self.search_in_log(reg) is None + assert self.get()['status'] == 200 + assert self.wait_for_record(reg) is not None, 'method GET' + + reg = r'^POST$' + assert self.search_in_log(reg) is None + assert self.post()['status'] == 200 + assert self.wait_for_record(reg) is not None, 'method POST' def test_variables_request_uri(self): - self.conf_routes("\"routes$request_uri\"") + self.set_format('$request_uri') + + def check_request_uri(req_uri): + reg = r'^' + re.escape(req_uri) + r'$' + + assert self.search_in_log(reg) is None + assert self.get(url=req_uri)['status'] == 200 + assert self.wait_for_record(reg) is not None - assert self.get(url='/3')['status'] == 203, 'request_uri' - assert self.get(url='/4*')['status'] == 204, 'request_uri 2' - assert self.get(url='/4%2A')['status'] == 204, 'request_uri 3' - assert self.get(url='/9?q#a')['status'] == 209, 'request_uri query' + check_request_uri('/3') + check_request_uri('/4*') + check_request_uri('/4%2A') + check_request_uri('/9?q#a') def test_variables_uri(self): - self.conf_routes("\"routes$uri\"") + self.set_format('$uri') - assert self.get(url='/3')['status'] == 203, 'uri' - assert self.get(url='/4*')['status'] == 204, 'uri 2' - assert self.get(url='/4%2A')['status'] == 204, 'uri 3' + def check_uri(uri, expect=None): + expect = uri if expect is None else expect + reg = r'^' + re.escape(expect) + r'$' + + assert self.search_in_log(reg) is None + assert self.get(url=uri)['status'] == 200 + assert self.wait_for_record(reg) is not None + + check_uri('/3') + check_uri('/4*') + check_uri('/5%2A', '/5*') + check_uri('/9?q#a', '/9') def test_variables_host(self): - self.conf_routes("\"routes/$host\"") + self.set_format('$host') + + def check_host(host, expect=None): + expect = host if expect is None else expect + reg = r'^' + re.escape(expect) + r'$' - def check_host(host, status=208): + assert self.search_in_log(reg) is None assert ( self.get(headers={'Host': host, 'Connection': 'close'})[ 'status' ] - == status + == 200 ) + assert self.wait_for_record(reg) is not None check_host('localhost') - check_host('localhost.') - check_host('localhost:7080') - check_host('.localhost', 404) - check_host('www.localhost', 404) - check_host('localhost1', 404) + check_host('localhost1.', 'localhost1') + check_host('localhost2:7080', 'localhost2') + check_host('.localhost') + check_host('www.localhost') def test_variables_remote_addr(self): - self.conf_routes("\"routes/$remote_addr\"") - assert self.get()['status'] == 211 + self.set_format('$remote_addr') + + assert self.get()['status'] == 200 + assert self.wait_for_record(r'^127\.0\.0\.1$') is not None assert 'success' in self.conf( - {"[::1]:7080": {"pass": "routes/$remote_addr"}}, 'listeners' + {"[::1]:7080": {"pass": "routes"}}, 'listeners' ) - assert self.get(sock_type='ipv6')['status'] == 212 + + reg = r'^::1$' + assert self.search_in_log(reg) is None + assert self.get(sock_type='ipv6')['status'] == 200 + assert self.wait_for_record(reg) is not None + + def test_variables_time_local(self): + self.set_format('$uri $time_local $uri') + + assert self.search_in_log(r'/time_local') is None + assert self.get(url='/time_local')['status'] == 200 + assert self.wait_for_record(r'/time_local') is not None, 'time log' + date = self.search_in_log( + r'^\/time_local (.*) \/time_local$', 'access.log' + )[1] + assert ( + abs( + self.date_to_sec_epoch(date, '%d/%b/%Y:%X %z') + - time.mktime(time.localtime()) + ) + < 5 + ), '$time_local' + + def test_variables_request_line(self): + self.set_format('$request_line') + + reg = r'^GET \/r_line HTTP\/1\.1$' + assert self.search_in_log(reg) is None + assert self.get(url='/r_line')['status'] == 200 + assert self.wait_for_record(reg) is not None + + def test_variables_status(self): + self.set_format('$status') + + assert 'success' in self.conf("418", 'routes/0/action/return') + + reg = r'^418$' + assert self.search_in_log(reg) is None + assert self.get()['status'] == 418 + assert self.wait_for_record(reg) is not None def test_variables_header_referer(self): - self.conf_routes("\"routes/$header_referer\"") + self.set_format('$method $header_referer') + + def check_referer(referer): + reg = r'^GET ' + re.escape(referer) + r'$' - def check_referer(referer, status=213): + assert self.search_in_log(reg) is None assert ( self.get( headers={ @@ -88,17 +207,21 @@ class TestVariables(TestApplicationProto): 'Referer': referer, } )['status'] - == status + == 200 ) + assert self.wait_for_record(reg) is not None check_referer('referer-value') - check_referer('', 404) - check_referer('no', 404) + check_referer('') + check_referer('no') def test_variables_header_user_agent(self): - self.conf_routes("\"routes/$header_user_agent\"") + self.set_format('$method $header_user_agent') - def check_user_agent(user_agent, status=214): + def check_user_agent(user_agent): + reg = r'^GET ' + re.escape(user_agent) + r'$' + + assert self.search_in_log(reg) is None assert ( self.get( headers={ @@ -107,152 +230,112 @@ class TestVariables(TestApplicationProto): 'User-Agent': user_agent, } )['status'] - == status + == 200 ) + assert self.wait_for_record(reg) is not None check_user_agent('MSIE') - check_user_agent('', 404) - check_user_agent('no', 404) - - def test_variables_dollar(self): - assert 'success' in self.conf( - { - "listeners": {"*:7080": {"pass": "routes"}}, - "routes": [{"action": {"return": 301}}], - } - ) - - def check_dollar(location, expect): - assert 'success' in self.conf( - '"' + location + '"', - 'routes/0/action/location', - ) - assert self.get()['headers']['Location'] == expect - - check_dollar( - 'https://${host}${uri}path${dollar}dollar', - 'https://localhost/path$dollar', - ) - check_dollar('path$dollar${dollar}', 'path$$') + check_user_agent('') + check_user_agent('no') def test_variables_many(self): - self.conf_routes("\"routes$uri$method\"") - assert self.get(url='/5')['status'] == 206, 'many' - - self.conf_routes("\"routes${uri}${method}\"") - assert self.get(url='/5')['status'] == 206, 'many 2' - - self.conf_routes("\"routes${uri}$method\"") - assert self.get(url='/5')['status'] == 206, 'many 3' + def check_vars(uri, expect): + reg = r'^' + re.escape(expect) + r'$' - self.conf_routes("\"routes/$method$method\"") - assert self.get()['status'] == 207, 'many 4' + assert self.search_in_log(reg) is None + assert self.get(url=uri)['status'] == 200 + assert self.wait_for_record(reg) is not None - self.conf_routes("\"routes/$method$uri\"") - assert self.get()['status'] == 404, 'no route' - assert self.get(url='/blah')['status'] == 404, 'no route 2' + self.set_format('$uri$method') + check_vars('/1', '/1GET') - def test_variables_replace(self): - assert self.get()['status'] == 201 + self.set_format('${uri}${method}') + check_vars('/2', '/2GET') - self.conf_routes("\"routes$uri\"") - assert self.get(url='/3')['status'] == 203 + self.set_format('${uri}$method') + check_vars('/3', '/3GET') - self.conf_routes("\"routes/${method}\"") - assert self.post()['status'] == 202 - - self.conf_routes("\"routes${uri}\"") - assert self.get(url='/4*')['status'] == 204 - - self.conf_routes("\"routes/blah$method}\"") - assert self.get()['status'] == 205 - - def test_variables_upstream(self): - assert 'success' in self.conf( - { - "listeners": { - "*:7080": {"pass": "upstreams$uri"}, - "*:7081": {"pass": "routes/one"}, - }, - "upstreams": {"1": {"servers": {"127.0.0.1:7081": {}}}}, - "routes": {"one": [{"action": {"return": 200}}]}, - }, - ), 'upstreams initial configuration' - - assert self.get(url='/1')['status'] == 200 - assert self.get(url='/2')['status'] == 404 - - def test_variables_empty(self): - def update_pass(prefix): - assert 'success' in self.conf( - {"listeners": {"*:7080": {"pass": prefix + "/$method"}}}, - ), 'variables empty' - - update_pass("routes") - assert self.get(url='/1')['status'] == 404 - - update_pass("upstreams") - assert self.get(url='/2')['status'] == 404 - - update_pass("applications") - assert self.get(url='/3')['status'] == 404 + self.set_format('$method$method') + check_vars('/', 'GETGET') def test_variables_dynamic(self): - self.conf_routes("\"routes/$header_foo$arg_foo$cookie_foo\"") + self.set_format('$header_foo$cookie_foo$arg_foo') + + assert ( + self.get( + url='/?foo=h', + headers={'Foo': 'b', 'Cookie': 'foo=la', 'Connection': 'close'}, + )['status'] + == 200 + ) + assert self.wait_for_record(r'^blah$') is not None - self.get( - url='/?foo=h', - headers={'Foo': 'b', 'Cookie': 'foo=la', 'Connection': 'close'}, - )['status'] = 210 + def test_variables_dynamic_arguments(self): + def check_arg(url, expect=None): + expect = url if expect is None else expect + reg = r'^' + re.escape(expect) + r'$' + + assert self.search_in_log(reg) is None + assert self.get(url=url)['status'] == 200 + assert self.wait_for_record(reg) is not None + + def check_no_arg(url): + assert self.get(url=url)['status'] == 200 + assert self.search_in_log(r'^0$') is None + + self.set_format('$arg_foo_bar') + check_arg('/?foo_bar=1', '1') + check_arg('/?foo_b%61r=2', '2') + check_arg('/?bar&foo_bar=3&foo', '3') + check_arg('/?foo_bar=l&foo_bar=4', '4') + check_no_arg('/') + check_no_arg('/?foo_bar=') + check_no_arg('/?Foo_bar=0') + check_no_arg('/?foo-bar=0') + check_no_arg('/?foo_bar=0&foo_bar=l') + + self.set_format('$arg_foo_b%61r') + check_no_arg('/?foo_b=0') + check_no_arg('/?foo_bar=0') + + self.set_format('$arg_f!~') + check_no_arg('/?f=0') + check_no_arg('/?f!~=0') def test_variables_dynamic_headers(self): - def check_header(header, status=210): + def check_header(header, value): + reg = r'^' + value + r'$' + + assert self.search_in_log(reg) is None assert ( - self.get(headers={header: "blah", 'Connection': 'close'})[ + self.get(headers={header: value, 'Connection': 'close'})[ 'status' ] - == status + == 200 ) + assert self.wait_for_record(reg) is not None - self.conf_routes("\"routes/$header_foo_bar\"") - check_header('foo-bar') - check_header('Foo-Bar') - check_header('foo_bar', 404) - check_header('Foo', 404) - check_header('Bar', 404) - check_header('foobar', 404) - - self.conf_routes("\"routes/$header_Foo_Bar\"") - check_header('Foo-Bar') - check_header('foo-bar') - check_header('foo_bar', 404) - check_header('foobar', 404) + def check_no_header(header): + assert ( + self.get(headers={header: '0', 'Connection': 'close'})['status'] + == 200 + ) + assert self.search_in_log(r'^0$') is None - self.conf_routes("\"routes/$header_foo-bar\"") - check_header('foo_bar', 404) + self.set_format('$header_foo_bar') + check_header('foo-bar', '1') + check_header('Foo-Bar', '2') + check_no_header('foo_bar') + check_no_header('foobar') - def test_variables_dynamic_arguments(self): - self.conf_routes("\"routes/$arg_foo_bar\"") - assert self.get(url='/?foo_bar=blah')['status'] == 210 - assert self.get(url='/?foo_b%61r=blah')['status'] == 210 - assert self.get(url='/?bar&foo_bar=blah&foo')['status'] == 210 - assert self.get(url='/?Foo_bar=blah')['status'] == 404 - assert self.get(url='/?foo-bar=blah')['status'] == 404 - assert self.get()['status'] == 404 - assert self.get(url='/?foo_bar=')['status'] == 404 - assert self.get(url='/?foo_bar=l&foo_bar=blah')['status'] == 210 - assert self.get(url='/?foo_bar=blah&foo_bar=l')['status'] == 404 - - self.conf_routes("\"routes/$arg_foo_b%61r\"") - assert self.get(url='/?foo_b=blah')['status'] == 404 - assert self.get(url='/?foo_bar=blah')['status'] == 404 - - self.conf_routes("\"routes/$arg_f!~\"") - assert self.get(url='/?f=blah')['status'] == 404 - assert self.get(url='/?f!~=blah')['status'] == 404 + self.set_format('$header_Foo_Bar') + check_header('Foo-Bar', '4') + check_header('foo-bar', '5') + check_no_header('foo_bar') + check_no_header('foobar') def test_variables_dynamic_cookies(self): - def check_cookie(cookie, status=210): + def check_no_cookie(cookie): assert ( self.get( headers={ @@ -261,33 +344,48 @@ class TestVariables(TestApplicationProto): 'Connection': 'close', }, )['status'] - == status - ), 'match cookie' + == 200 + ) + assert self.search_in_log(r'^0$') is None + + self.set_format('$cookie_foo_bar') + + reg = r'^1$' + assert self.search_in_log(reg) is None + self.get( + headers={ + 'Host': 'localhost', + 'Cookie': 'foo_bar=1', + 'Connection': 'close', + }, + )['status'] == 200 + assert self.wait_for_record(reg) is not None - self.conf_routes("\"routes/$cookie_foo_bar\"") - check_cookie('foo_bar=blah', 210) - check_cookie('fOo_bar=blah', 404) - assert self.get()['status'] == 404 - check_cookie('foo_bar', 404) - check_cookie('foo_bar=', 404) + check_no_cookie('fOo_bar=0') + check_no_cookie('foo_bar=') def test_variables_invalid(self): - def check_variables(routes): + def check_variables(format): assert 'error' in self.conf( - routes, 'listeners/*:7080/pass' - ), 'invalid variables' - - check_variables("\"routes$\"") - check_variables("\"routes${\"") - check_variables("\"routes${}\"") - check_variables("\"routes$ur\"") - check_variables("\"routes$uriblah\"") - check_variables("\"routes${uri\"") - check_variables("\"routes${{uri}\"") - check_variables("\"routes$ar\"") - check_variables("\"routes$arg\"") - check_variables("\"routes$arg_\"") - check_variables("\"routes$cookie\"") - check_variables("\"routes$cookie_\"") - check_variables("\"routes$header\"") - check_variables("\"routes$header_\"") + { + 'path': option.temp_dir + '/access.log', + 'format': format, + }, + 'access_log', + ), 'access_log format' + + check_variables("$") + check_variables("${") + check_variables("${}") + check_variables("$ur") + check_variables("$uri$$host") + check_variables("$uriblah") + check_variables("${uri") + check_variables("${{uri}") + check_variables("$ar") + check_variables("$arg") + check_variables("$arg_") + check_variables("$cookie") + check_variables("$cookie_") + check_variables("$header") + check_variables("$header_") diff --git a/test/unit/applications/lang/go.py b/test/unit/applications/lang/go.py index 3db955f3..14e76362 100644 --- a/test/unit/applications/lang/go.py +++ b/test/unit/applications/lang/go.py @@ -67,7 +67,9 @@ replace unit.nginx.org/go => {replace_path} print("\n$ GOPATH=" + env['GOPATH'] + " " + " ".join(args)) try: - process = subprocess.run(args, env=env, cwd=temp_dir) + output = subprocess.check_output( + args, env=env, cwd=temp_dir, stderr=subprocess.STDOUT + ) except KeyboardInterrupt: raise @@ -75,7 +77,7 @@ replace unit.nginx.org/go => {replace_path} except subprocess.CalledProcessError: return None - return process + return output def load(self, script, name='app', **kwargs): static_build = False diff --git a/test/unit/applications/lang/java.py b/test/unit/applications/lang/java.py index 50998978..c8936274 100644 --- a/test/unit/applications/lang/java.py +++ b/test/unit/applications/lang/java.py @@ -52,7 +52,7 @@ class TestApplicationJava(TestApplicationProto): os.makedirs(classes_path) classpath = ( - option.current_dir + '/build/tomcat-servlet-api-9.0.52.jar' + option.current_dir + '/build/tomcat-servlet-api-9.0.70.jar' ) ws_jars = glob.glob( diff --git a/test/unit/applications/lang/python.py b/test/unit/applications/lang/python.py index 1e38f3fa..3768cf07 100644 --- a/test/unit/applications/lang/python.py +++ b/test/unit/applications/lang/python.py @@ -50,6 +50,7 @@ class TestApplicationPython(TestApplicationProto): 'protocol', 'targets', 'threads', + 'prefix', ): if attr in kwargs: app[attr] = kwargs.pop(attr) diff --git a/test/unit/applications/websockets.py b/test/unit/applications/websockets.py index d647ce9b..15f212ff 100644 --- a/test/unit/applications/websockets.py +++ b/test/unit/applications/websockets.py @@ -43,10 +43,9 @@ class TestApplicationWebsocket(TestApplicationProto): 'Sec-WebSocket-Version': 13, } - _, sock = self.get( + sock = self.get( headers=headers, no_recv=True, - start=True, ) resp = '' diff --git a/test/unit/check/go.py b/test/unit/check/go.py index 3d9d13e7..09ae641d 100644 --- a/test/unit/check/go.py +++ b/test/unit/check/go.py @@ -2,7 +2,5 @@ from unit.applications.lang.go import TestApplicationGo def check_go(): - process = TestApplicationGo.prepare_env('empty') - - if process != None and process.returncode == 0: + if TestApplicationGo.prepare_env('empty') is not None: return True diff --git a/test/unit/check/njs.py b/test/unit/check/njs.py new file mode 100644 index 00000000..433473a1 --- /dev/null +++ b/test/unit/check/njs.py @@ -0,0 +1,6 @@ +import re + + +def check_njs(output_version): + if re.search('--njs', output_version): + return True diff --git a/test/unit/check/regex.py b/test/unit/check/regex.py index 734c0150..51cf966b 100644 --- a/test/unit/check/regex.py +++ b/test/unit/check/regex.py @@ -1,13 +1,8 @@ import re -import subprocess -def check_regex(unitd): - output = subprocess.check_output( - [unitd, '--version'], stderr=subprocess.STDOUT - ) - - if re.search('--no-regex', output.decode()): +def check_regex(output_version): + if re.search('--no-regex', output_version): return False return True diff --git a/test/unit/check/tls.py b/test/unit/check/tls.py index b878ff7d..53ce5ffc 100644 --- a/test/unit/check/tls.py +++ b/test/unit/check/tls.py @@ -2,12 +2,11 @@ import re import subprocess -def check_openssl(unitd): - subprocess.check_output(['which', 'openssl']) +def check_openssl(output_version): + try: + subprocess.check_output(['which', 'openssl']) + except subprocess.CalledProcessError: + return None - output = subprocess.check_output( - [unitd, '--version'], stderr=subprocess.STDOUT - ) - - if re.search('--openssl', output.decode()): + if re.search('--openssl', output_version): return True diff --git a/test/unit/http.py b/test/unit/http.py index b29667c9..c48a720f 100644 --- a/test/unit/http.py +++ b/test/unit/http.py @@ -102,7 +102,12 @@ class TestHTTP: if 'read_buffer_size' in kwargs: recvall_kwargs['buff_size'] = kwargs['read_buffer_size'] - resp = self.recvall(sock, **recvall_kwargs).decode(encoding) + resp = self.recvall(sock, **recvall_kwargs).decode( + encoding, errors='ignore' + ) + + else: + return sock self.log_in(resp) diff --git a/test/unit/utils.py b/test/unit/utils.py index 43aaa81b..d6590b97 100644 --- a/test/unit/utils.py +++ b/test/unit/utils.py @@ -12,9 +12,15 @@ def public_dir(path): for root, dirs, files in os.walk(path): for d in dirs: - os.chmod(os.path.join(root, d), 0o777) + try: + os.chmod(os.path.join(root, d), 0o777) + except FileNotFoundError: + pass for f in files: - os.chmod(os.path.join(root, f), 0o777) + try: + os.chmod(os.path.join(root, f), 0o777) + except FileNotFoundError: + pass def waitforfiles(*files, timeout=50): @@ -66,12 +72,19 @@ def waitforsocket(port): pytest.fail('Can\'t connect to the 127.0.0.1:' + str(port)) -def findmnt(): +def check_findmnt(): try: - out = subprocess.check_output( + return subprocess.check_output( ['findmnt', '--raw'], stderr=subprocess.STDOUT ).decode() except FileNotFoundError: + return False + + +def findmnt(): + out = check_findmnt() + + if not out: pytest.skip('requires findmnt') return out |