From 76df62a6236eba2ae1ea7ffe7b9599418b044a01 Mon Sep 17 00:00:00 2001 From: Zhidao HONG Date: Mon, 19 Sep 2022 02:45:44 +0800 Subject: HTTP: fixed cookie parsing. The fixing supports the cookie value with the '=' character. This is related to #756 PR on Github. Thanks to changxiaocui. --- test/test_routing.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'test') diff --git a/test/test_routing.py b/test/test_routing.py index 3649b37c..0d7b908c 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"}}) -- cgit From 97fa587c9f80ecee7bd2e2be173e700d011b09b9 Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Tue, 27 Sep 2022 12:08:36 +0100 Subject: Tests: added test with proxy for status. --- test/test_status.py | 95 ++++++++++++++++++++++++++++------------------------- 1 file changed, 51 insertions(+), 44 deletions(-) (limited to 'test') diff --git a/test/test_status.py b/test/test_status.py index 214072d4..4c5e56ab 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}, @@ -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) + 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' -- cgit From 8e1e0471914b39da0634a23c4231ce98021a4cf7 Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Tue, 11 Oct 2022 13:49:10 +0100 Subject: Tests: don't try to return response when "no_recv" is True. --- test/test_asgi_application.py | 6 ++---- test/test_java_application.py | 3 +-- test/test_perl_application.py | 3 +-- test/test_proxy.py | 25 +++++++------------------ test/test_python_application.py | 3 +-- test/test_reconfigure.py | 5 ++--- test/test_routing.py | 2 +- test/test_ruby_application.py | 3 +-- test/test_settings.py | 8 +++----- test/test_static.py | 4 ++-- test/test_status.py | 4 ++-- test/test_upstreams_rr.py | 8 +++----- test/unit/applications/websockets.py | 3 +-- test/unit/http.py | 3 +++ 14 files changed, 30 insertions(+), 50 deletions(-) (limited to 'test') diff --git a/test/test_asgi_application.py b/test/test_asgi_application.py index 34dfe18e..7f5f1578 100644 --- a/test/test_asgi_application.py +++ b/test/test_asgi_application.py @@ -277,10 +277,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 +357,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_java_application.py b/test/test_java_application.py index adcb4eca..b4dbff59 100644 --- a/test/test_java_application.py +++ b/test/test_java_application.py @@ -1001,14 +1001,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_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_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..946d2118 100644 --- a/test/test_python_application.py +++ b/test/test_python_application.py @@ -765,14 +765,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_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 0d7b908c..9e872061 100644 --- a/test/test_routing.py +++ b/test/test_routing.py @@ -1485,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..1bc9a432 100644 --- a/test/test_settings.py +++ b/test/test_settings.py @@ -50,20 +50,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 +243,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..43d8b7cc 100644 --- a/test/test_static.py +++ b/test/test_static.py @@ -262,8 +262,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 4c5e56ab..6c733474 100644 --- a/test/test_status.py +++ b/test/test_status.py @@ -81,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) @@ -112,7 +112,7 @@ Connection: close # idle - _, sock = self.http(b'', start=True, raw=True, no_recv=True) + sock = self.http(b'', raw=True, no_recv=True) self.check_connections(2, 0, 1, 1) self.get(sock=sock) 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/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/http.py b/test/unit/http.py index b29667c9..144f300c 100644 --- a/test/unit/http.py +++ b/test/unit/http.py @@ -104,6 +104,9 @@ class TestHTTP: resp = self.recvall(sock, **recvall_kwargs).decode(encoding) + else: + return sock + self.log_in(resp) if 'raw_resp' not in kwargs: -- cgit From 08dab702cb4140613e942b86f54824b3c7dea2ad Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Tue, 11 Oct 2022 13:49:44 +0100 Subject: Tests: reworked "test_variables.py". Access log used for the variables testing instead of limited routing. Added missed test for $status variable. Some tests moved from "test_access_log.py" to "test_variables.py". --- test/test_access_log.py | 22 --- test/test_variables.py | 453 +++++++++++++++++++++++++++--------------------- 2 files changed, 259 insertions(+), 216 deletions(-) (limited to 'test') 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_variables.py b/test/test_variables.py index 2ddfdc0a..43c5c471 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,165 @@ 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_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') - 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' + 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 + + 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') + + def check_uri(uri, expect=None): + expect = uri if expect is None else expect + reg = r'^' + re.escape(expect) + r'$' - 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' + 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 +178,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 +201,109 @@ 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') self.get( url='/?foo=h', headers={'Foo': 'b', 'Cookie': 'foo=la', 'Connection': 'close'}, - )['status'] = 210 + )['status'] = 200 + assert self.wait_for_record(r'^blah$') is not None + + 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) + 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') - check_header('foo-bar') - check_header('foo_bar', 404) - check_header('foobar', 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') - self.conf_routes("\"routes/$header_foo-bar\"") - check_header('foo_bar', 404) - - 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 +312,47 @@ class TestVariables(TestApplicationProto): 'Connection': 'close', }, )['status'] - == status - ), 'match cookie' + == 200 + ) + assert self.search_in_log(r'^0$') is 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) + 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 + + 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("$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_") -- cgit From af5903ff4ee83a0c632a07828ed80a0b5f40aefd Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Thu, 13 Oct 2022 10:13:57 +0100 Subject: Tests: added tests for the $request_time variable. --- test/test_variables.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) (limited to 'test') diff --git a/test/test_variables.py b/test/test_variables.py index 43c5c471..ebbc5aa2 100644 --- a/test/test_variables.py +++ b/test/test_variables.py @@ -47,6 +47,35 @@ class TestVariables(TestApplicationProto): ) 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): self.set_format('$method') -- cgit From a03274456b54cbc39e220b9dd73c3fc3fb935e46 Mon Sep 17 00:00:00 2001 From: Andrew Clayton Date: Fri, 16 Sep 2022 14:38:53 +0100 Subject: PHP: allowed to specify URLs without a trailing '/'. Both @lucatacconi & @mwoodpatrick reported what appears to be the same issue on GitHub. Namely that when using the PHP language module and trying to access a URL that is a directory but without specifying the trailing '/', they were getting a '503 Service Unavailable' error. Note: This is when _not_ using the 'script' option. E.g with the following config { "listeners": { "[::1]:8080": { "pass": "applications/php" } }, "applications": { "php": { "type": "php", "root": "/var/tmp/unit-php" } } } and with a directory path of /var/tmp/unit-php/foo containing an index.php, you would see the following $ curl http://localhost/foo Error 503 Error 503 However $ curl http://localhost/foo/ would work and serve up the index.php This commit fixes the above so you get the desired behaviour without specifying the trailing '/' by doing the following 1] If the URL doesn't end in .php and doesn't have a trailing '/' then check if the requested path is a directory. 2) If it is a directory then create a 301 re-direct pointing to it. This matches the behaviour of the likes of nginx, Apache and lighttpd. This also matches the behaviour of the "share" action in Unit. This doesn't effect the behaviour of the 'script' option which bypasses the nxt_php_dynamic_request() function. This also adds a couple of tests to test/test_php_application.py to ensure this continues to work. Closes: Closes: Signed-off-by: Andrew Clayton --- test/test_php_application.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) (limited to 'test') 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('') + + 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') -- cgit From 0d3b31e6710afe4348eb25f1602f5271c92b9a77 Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Tue, 15 Nov 2022 00:39:21 +0000 Subject: Tests: features and options checks improved. Now version output evaluates only once. OpenSSL checks more carefully. --- test/conftest.py | 7 +++++-- test/unit/check/regex.py | 9 ++------- test/unit/check/tls.py | 13 ++++++------- 3 files changed, 13 insertions(+), 16 deletions(-) (limited to 'test') diff --git a/test/conftest.py b/test/conftest.py index 18851baa..ac88675a 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -176,6 +176,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 +205,10 @@ 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']['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 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 -- cgit From 2c2156e236c18a67922617a4e43fb9eb1da0ab9e Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Tue, 15 Nov 2022 00:42:12 +0000 Subject: Tests: fixed assertion in test_variables_dynamic. --- test/test_variables.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'test') diff --git a/test/test_variables.py b/test/test_variables.py index ebbc5aa2..80236f65 100644 --- a/test/test_variables.py +++ b/test/test_variables.py @@ -261,10 +261,13 @@ Connection: close def test_variables_dynamic(self): self.set_format('$header_foo$cookie_foo$arg_foo') - self.get( - url='/?foo=h', - headers={'Foo': 'b', 'Cookie': 'foo=la', 'Connection': 'close'}, - )['status'] = 200 + 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 def test_variables_dynamic_arguments(self): -- cgit From bb11ef694ce49343a11bafee836addb6dd125848 Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Tue, 15 Nov 2022 00:56:49 +0000 Subject: Tests: removed migration test. Migration of "share" behaviour was dropped after b57b4749b993. --- test/test_static.py | 46 ---------------------------------------------- 1 file changed, 46 deletions(-) (limited to 'test') diff --git a/test/test_static.py b/test/test_static.py index 43d8b7cc..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( -- cgit From 9ea5ed2813c7dc57c8997ef21d779baae19d784c Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Tue, 15 Nov 2022 01:07:41 +0000 Subject: Tests: fixed _check_processes() checks in "--restart" mode. --- test/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'test') diff --git a/test/conftest.py b/test/conftest.py index ac88675a..bf2951db 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -574,6 +574,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] -- cgit From 3711632c00f8538fd87ff1a14a028756f57239a2 Mon Sep 17 00:00:00 2001 From: Zhidao HONG Date: Sun, 20 Nov 2022 23:11:41 +0800 Subject: Var: improved variable parsing with empty names. Unit parsed the case of "$uri$$host" into unknown variables. This commit makes it invalid variable instead. --- test/test_variables.py | 1 + 1 file changed, 1 insertion(+) (limited to 'test') diff --git a/test/test_variables.py b/test/test_variables.py index 80236f65..ecce5e6d 100644 --- a/test/test_variables.py +++ b/test/test_variables.py @@ -378,6 +378,7 @@ Connection: close check_variables("${") check_variables("${}") check_variables("$ur") + check_variables("$uri$$host") check_variables("$uriblah") check_variables("${uri") check_variables("${{uri}") -- cgit From 190691ade82f5126271b374dd5b8d0cb57f9473a Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Tue, 29 Nov 2022 01:02:08 +0000 Subject: Tests: NJS. --- test/conftest.py | 2 ++ test/test_njs.py | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++ test/unit/check/njs.py | 6 ++++ test/unit/http.py | 4 ++- 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 test/test_njs.py create mode 100644 test/unit/check/njs.py (limited to 'test') diff --git a/test/conftest.py b/test/conftest.py index bf2951db..759f11bd 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 @@ -206,6 +207,7 @@ def pytest_sessionstart(session): # discover modules from check 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']['openssl'] = check_openssl(output_version) option.available['modules']['regex'] = check_regex(output_version) 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/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/http.py b/test/unit/http.py index 144f300c..c48a720f 100644 --- a/test/unit/http.py +++ b/test/unit/http.py @@ -102,7 +102,9 @@ 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 -- cgit From 55b9a5307d705da91d3ef317639356c748853a7c Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Fri, 9 Dec 2022 14:17:49 +0000 Subject: Tests: fixed tests to run as privileged user. --- test/test_python_isolation.py | 13 +++++++------ test/unit/utils.py | 10 ++++++++-- 2 files changed, 15 insertions(+), 8 deletions(-) (limited to 'test') diff --git a/test/test_python_isolation.py b/test/test_python_isolation.py index 8cef6812..db398444 100644 --- a/test/test_python_isolation.py +++ b/test/test_python_isolation.py @@ -63,24 +63,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: diff --git a/test/unit/utils.py b/test/unit/utils.py index 43aaa81b..f9e9d08a 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): -- cgit From 648e91a623d3822e8ab4780b452da211ea3ba257 Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Mon, 12 Dec 2022 16:24:54 +0000 Subject: Tests: pretty output. Hide expected alerts by default. Silence succesfull "go build" information. --- test/conftest.py | 9 +++++---- test/unit/applications/lang/go.py | 6 ++++-- test/unit/check/go.py | 4 +--- 3 files changed, 10 insertions(+), 9 deletions(-) (limited to 'test') diff --git a/test/conftest.py b/test/conftest.py index 759f11bd..c2680744 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -485,14 +485,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] @@ -504,7 +505,7 @@ def _check_alerts(*, log=None): assert not sanitizer_errors, 'sanitizer error(s)' - if found: + if found and option.detailed: print('skipped.') 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/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 -- cgit From 12e2cbae8a0bf190c8e7d98de6c08aff57d2ae4f Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Mon, 12 Dec 2022 16:27:02 +0000 Subject: Tests: stop execution if can't unmount any filesystem. --- test/conftest.py | 6 ++++++ test/unit/utils.py | 11 +++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) (limited to 'test') diff --git a/test/conftest.py b/test/conftest.py index c2680744..4a1aa7cc 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -26,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): @@ -87,6 +89,7 @@ _fds_info = { }, } http = TestHTTP() +is_findmnt = check_findmnt() def pytest_configure(config): @@ -315,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', diff --git a/test/unit/utils.py b/test/unit/utils.py index f9e9d08a..d6590b97 100644 --- a/test/unit/utils.py +++ b/test/unit/utils.py @@ -72,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 -- cgit From 6313cffd26bf88009a871361f3da56444def0288 Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Tue, 13 Dec 2022 14:51:11 +0000 Subject: Tests: added tests for the large header buffer settings. Added tests for the "large_header_buffer_size" and "large_header_buffers" configuration options. --- test/test_settings.py | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) (limited to 'test') diff --git a/test/test_settings.py b/test/test_settings.py index 1bc9a432..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') -- cgit From c9c001ee16091c76773d3e9e655d777696dd755a Mon Sep 17 00:00:00 2001 From: "Sergey A. Osokin" Date: Wed, 14 Dec 2022 01:43:24 +0000 Subject: Java: upgrading third-party components. --- test/test_java_application.py | 17 ++++++++++------- test/unit/applications/lang/java.py | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) (limited to 'test') diff --git a/test/test_java_application.py b/test/test_java_application.py index b4dbff59..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') 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( -- cgit From 6dae517ebd20baa2066541e703d6aa594326dd69 Mon Sep 17 00:00:00 2001 From: OutOfFocus4 Date: Sun, 14 Nov 2021 10:47:07 -0500 Subject: Python: Added "prefix" to configuration. This patch gives users the option to set a `"prefix"` attribute for Python applications, either at the top level or for specific `"target"`s. If the attribute is present, the value of `"prefix"` must be a string beginning with `"/"`. If the value of the `"prefix"` attribute is longer than 1 character and ends in `"/"`, the trailing `"/"` is stripped. The purpose of the `"prefix"` attribute is to set the `SCRIPT_NAME` context value for WSGI applications and the `root_path` context value for ASGI applications, allowing applications to properly route requests regardless of the path that the server uses to expose the application. The context value is only set if the request's URL path begins with the value of the `"prefix"` attribute. In all other cases, the `SCRIPT_NAME` or `root_path` values are not set. In addition, for WSGI applications, the value of `"prefix"` will be stripped from the beginning of the request's URL path before it is sent to the application. Reviewed-by: Andrei Zeliankou Reviewed-by: Artem Konev Signed-off-by: Alejandro Colomar --- test/python/prefix/asgi.py | 15 ++++++ test/python/prefix/wsgi.py | 10 ++++ test/python/targets/asgi.py | 17 +++++++ test/python/targets/wsgi.py | 9 ++++ test/test_asgi_application.py | 37 +++++++++++++++ test/test_asgi_targets.py | 45 ++++++++++++++++++ test/test_configuration.py | 86 +++++++++++++++++++++++++++++++++++ test/test_python_application.py | 38 ++++++++++++++++ test/test_python_targets.py | 52 +++++++++++++++++++++ test/unit/applications/lang/python.py | 1 + 10 files changed, 310 insertions(+) create mode 100644 test/python/prefix/asgi.py create mode 100644 test/python/prefix/wsgi.py (limited to 'test') 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_asgi_application.py b/test/test_asgi_application.py index 7f5f1578..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') 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_python_application.py b/test/test_python_application.py index 946d2118..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') 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/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) -- cgit From 4b39cb1fc75b42e944e41a522dbdf3d539eff8db Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Wed, 14 Dec 2022 19:00:14 +0000 Subject: Tests: added tests for "path" option in isolation/cgroup. --- test/test_python_isolation.py | 123 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) (limited to 'test') diff --git a/test/test_python_isolation.py b/test/test_python_isolation.py index db398444..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() @@ -102,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') -- cgit