CI / Build Pip package (push) Successful in 4m8s
Instrument HTTP requests and WebSocket connections via Kaya hooks, covering both ASGI and RSGI. Records handler exceptions, WebSocket close codes, optional header capture, exclusions and lifecycle hooks. Add route-template resolution and exception visibility to kaya-core so trace/metric attributes can use low-cardinality routes and failed spans can record escaped exceptions.
210 lines
11 KiB
Python
210 lines
11 KiB
Python
from typing import Sequence, Tuple, Optional, List
|
|
|
|
from kaya.core import Tree, PathHandler, HttpContext, HttpMethod, PathIterator
|
|
from pwo import Maybe
|
|
import unittest
|
|
|
|
|
|
class PathIteratorTest(unittest.TestCase):
|
|
cases: Tuple[Tuple[str, Tuple[str, ...]], ...] = (
|
|
('/', tuple()),
|
|
('root/foo', ('root', 'foo')),
|
|
('/root', ('root',)),
|
|
('/root', ('root',)),
|
|
('/root/', ('root',)),
|
|
('/root/bar/', ('root', 'bar')),
|
|
)
|
|
|
|
def test_path_iterator(self):
|
|
for (case, expected) in self.cases:
|
|
with self.subTest(case) as _:
|
|
components = tuple((c for c in PathIterator(case)))
|
|
self.assertEqual(expected, components)
|
|
|
|
|
|
class TreeTest(unittest.TestCase):
|
|
tree: Tree
|
|
handlers: List[PathHandler]
|
|
|
|
def setUp(self):
|
|
self.tree = Tree()
|
|
|
|
class TestHandler(PathHandler):
|
|
|
|
def handle_request(self, ctx: HttpContext):
|
|
pass
|
|
|
|
@property
|
|
def recursive(self) -> bool:
|
|
return True
|
|
|
|
self.handlers = [TestHandler() for _ in range(20)]
|
|
|
|
routes: Tuple[Tuple[Tuple[str, ...], Optional[HttpMethod], PathHandler], ...] = (
|
|
(('home', 'something'), HttpMethod.GET, self.handlers[0]),
|
|
(('home', 'something_else'), HttpMethod.GET, self.handlers[1]),
|
|
(('home', 'something_else'), HttpMethod.POST, self.handlers[2]),
|
|
(('home', 'something', 'object'), HttpMethod.GET, self.handlers[3]),
|
|
(('home', 'something_else', 'foo'), HttpMethod.GET, self.handlers[4]),
|
|
(('home',), HttpMethod.GET, self.handlers[5]),
|
|
(('home',), HttpMethod.POST, self.handlers[6]),
|
|
(('home',), None, self.handlers[7]),
|
|
(('home', '*.md'), None, self.handlers[8]),
|
|
(('home', 'something', '*', 'blah', '*.md'), None, self.handlers[9]),
|
|
(('home', 'bar', '*'), None, self.handlers[10]),
|
|
|
|
)
|
|
|
|
for path, method, handler in routes:
|
|
self.tree.add((p for p in path), method, handler)
|
|
|
|
def test_tree(self):
|
|
|
|
cases: Tuple[Tuple[str, HttpMethod, Optional[int]], ...] = (
|
|
('http://localhost:127.0.0.1:5432/home/something', HttpMethod.GET, 0),
|
|
('http://localhost:127.0.0.1:5432/home/something_else', HttpMethod.GET, 1),
|
|
('http://localhost:127.0.0.1:5432/home/something_else', HttpMethod.POST, 2),
|
|
('http://localhost:127.0.0.1:5432/home/something/object', HttpMethod.GET, 3),
|
|
('http://localhost:127.0.0.1:5432/home/something_else/foo', HttpMethod.GET, 4),
|
|
('http://localhost:127.0.0.1:5432/', HttpMethod.GET, None),
|
|
('http://localhost:127.0.0.1:5432/home', HttpMethod.GET, 5),
|
|
('http://localhost:127.0.0.1:5432/home', HttpMethod.POST, 6),
|
|
('http://localhost:127.0.0.1:5432/home', HttpMethod.PUT, 7),
|
|
('http://localhost:127.0.0.1:5432/home/README.md', HttpMethod.GET, 8),
|
|
('http://localhost:127.0.0.1:5432/home/something/ciao/blah/README.md', HttpMethod.GET, 9),
|
|
('http://localhost:127.0.0.1:5432/home/bar/ciao/blah/README.md', HttpMethod.GET, 10),
|
|
)
|
|
for url, method, handler_num in cases:
|
|
with self.subTest(f"{str(method)} {url}"):
|
|
res = self.tree.get_handler(url, method)
|
|
self.assertIs(Maybe.of(handler_num).map(self.handlers.__getitem__).or_none(),
|
|
Maybe.of_nullable(res).map(lambda it: it[0]).or_none())
|
|
|
|
def test_route_template(self):
|
|
cases: Tuple[Tuple[str, HttpMethod, Optional[str]], ...] = (
|
|
('/home/something', HttpMethod.GET, '/home/something'),
|
|
('/home/something_else', HttpMethod.POST, '/home/something_else'),
|
|
('/home/README.md', HttpMethod.GET, '/home/*.md'),
|
|
('/home/something/ciao/blah/README.md', HttpMethod.GET, '/home/something/*/blah/*.md'),
|
|
('/home/bar/ciao/blah/README.md', HttpMethod.GET, '/home/bar/*'),
|
|
('/unknown', HttpMethod.GET, None),
|
|
)
|
|
for url, method, expected in cases:
|
|
with self.subTest(f'{method} {url}'):
|
|
self.assertEqual(expected, self.tree.route_template(url, method))
|
|
|
|
def test_route_template_with_params(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('foo', '${id:int}')), HttpMethod.PUT, self.handlers[0])
|
|
tree.add((p for p in ('foo', '${name}')), HttpMethod.GET, self.handlers[1])
|
|
self.assertEqual('/foo/${id:int}', tree.route_template('/foo/42', HttpMethod.PUT))
|
|
self.assertEqual('/foo/${name}', tree.route_template('/foo/bar', HttpMethod.GET))
|
|
self.assertIsNone(tree.route_template('/foo/bar', HttpMethod.DELETE))
|
|
|
|
def test_two_method_agnostic_matchers_raise(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('foo', '*')), None, self.handlers[0])
|
|
with self.assertRaises(ValueError):
|
|
tree.add((p for p in ('foo', '*.md')), None, self.handlers[1])
|
|
|
|
def test_identical_method_agnostic_matchers_reuse(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('foo', '*')), None, self.handlers[0])
|
|
tree.add((p for p in ('foo', '*')), None, self.handlers[1])
|
|
handler = Maybe.of_nullable(tree.get_handler('/foo/bar', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
self.assertIs(self.handlers[0], handler)
|
|
|
|
def test_two_overlapping_method_specific_matchers_raise(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('foo', '${id:int}')), HttpMethod.PUT, self.handlers[0])
|
|
with self.assertRaises(ValueError):
|
|
tree.add((p for p in ('foo', '${name:str}')), HttpMethod.PUT, self.handlers[1])
|
|
|
|
def test_disjoint_method_specific_matchers_allowed(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('foo', '*')), HttpMethod.PUT, self.handlers[0])
|
|
tree.add((p for p in ('foo', '*')), HttpMethod.GET, self.handlers[1])
|
|
put_handler = Maybe.of_nullable(tree.get_handler('/foo/bar', HttpMethod.PUT)).map(lambda it: it[0]).or_none()
|
|
get_handler = Maybe.of_nullable(tree.get_handler('/foo/bar', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
self.assertIs(self.handlers[0], put_handler)
|
|
self.assertIs(self.handlers[1], get_handler)
|
|
|
|
def test_nested_routes_with_same_param_allowed(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('restaurants', '${id}')), HttpMethod.GET, self.handlers[0])
|
|
tree.add((p for p in ('restaurants', '${id}', 'menu')), HttpMethod.GET, self.handlers[1])
|
|
h0 = Maybe.of_nullable(tree.get_handler('/restaurants/42', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
h1 = Maybe.of_nullable(tree.get_handler('/restaurants/42/menu', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
self.assertIs(self.handlers[0], h0)
|
|
self.assertIs(self.handlers[1], h1)
|
|
|
|
def test_nested_routes_same_param_reverse_order(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('restaurants', '${id}', 'menu')), HttpMethod.GET, self.handlers[1])
|
|
tree.add((p for p in ('restaurants', '${id}')), HttpMethod.GET, self.handlers[0])
|
|
h0 = Maybe.of_nullable(tree.get_handler('/restaurants/42', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
h1 = Maybe.of_nullable(tree.get_handler('/restaurants/42/menu', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
self.assertIs(self.handlers[0], h0)
|
|
self.assertIs(self.handlers[1], h1)
|
|
|
|
def test_nested_routes_with_same_int_param_allowed(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('restaurants', '${id:int}')), HttpMethod.GET, self.handlers[0])
|
|
tree.add((p for p in ('restaurants', '${id:int}', 'menu')), HttpMethod.GET, self.handlers[1])
|
|
h0 = Maybe.of_nullable(tree.get_handler('/restaurants/42', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
h1 = Maybe.of_nullable(tree.get_handler('/restaurants/42/menu', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
self.assertIs(self.handlers[0], h0)
|
|
self.assertIs(self.handlers[1], h1)
|
|
|
|
def test_nested_routes_method_agnostic_reuses_matcher(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('restaurants', '${id}')), HttpMethod.GET, self.handlers[0])
|
|
tree.add((p for p in ('restaurants', '${id}', 'menu')), None, self.handlers[1])
|
|
h0 = Maybe.of_nullable(tree.get_handler('/restaurants/42', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
h1 = Maybe.of_nullable(tree.get_handler('/restaurants/42/menu', HttpMethod.POST)).map(lambda it: it[0]).or_none()
|
|
self.assertIs(self.handlers[0], h0)
|
|
self.assertIs(self.handlers[1], h1)
|
|
|
|
def test_different_param_names_still_raise(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('a', '${id}')), HttpMethod.GET, self.handlers[0])
|
|
with self.assertRaises(ValueError):
|
|
tree.add((p for p in ('a', '${name}', 'x')), HttpMethod.GET, self.handlers[1])
|
|
|
|
def test_different_param_kinds_still_raise(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('a', '${id}')), HttpMethod.GET, self.handlers[0])
|
|
with self.assertRaises(ValueError):
|
|
tree.add((p for p in ('a', '${id:int}', 'x')), HttpMethod.GET, self.handlers[1])
|
|
|
|
def test_nested_routes_different_methods_share_subtree(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('restaurants', '${id}', 'menu')), HttpMethod.GET, self.handlers[0])
|
|
tree.add((p for p in ('restaurants', '${id}', 'menu')), HttpMethod.POST, self.handlers[1])
|
|
h0 = Maybe.of_nullable(tree.get_handler('/restaurants/42/menu', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
h1 = Maybe.of_nullable(tree.get_handler('/restaurants/42/menu', HttpMethod.POST)).map(lambda it: it[0]).or_none()
|
|
self.assertIs(self.handlers[0], h0)
|
|
self.assertIs(self.handlers[1], h1)
|
|
|
|
def test_nested_routes_different_methods_reverse_order(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('restaurants', '${id}', 'menu')), HttpMethod.POST, self.handlers[1])
|
|
tree.add((p for p in ('restaurants', '${id}', 'menu')), HttpMethod.GET, self.handlers[0])
|
|
h0 = Maybe.of_nullable(tree.get_handler('/restaurants/42/menu', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
h1 = Maybe.of_nullable(tree.get_handler('/restaurants/42/menu', HttpMethod.POST)).map(lambda it: it[0]).or_none()
|
|
self.assertIs(self.handlers[0], h0)
|
|
self.assertIs(self.handlers[1], h1)
|
|
|
|
def test_nested_routes_combined_detail_and_menu_methods(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('restaurants', '${id}')), HttpMethod.GET, self.handlers[0])
|
|
tree.add((p for p in ('restaurants', '${id}', 'menu')), HttpMethod.GET, self.handlers[1])
|
|
tree.add((p for p in ('restaurants', '${id}', 'menu')), HttpMethod.POST, self.handlers[2])
|
|
h0 = Maybe.of_nullable(tree.get_handler('/restaurants/42', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
h1 = Maybe.of_nullable(tree.get_handler('/restaurants/42/menu', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
h2 = Maybe.of_nullable(tree.get_handler('/restaurants/42/menu', HttpMethod.POST)).map(lambda it: it[0]).or_none()
|
|
self.assertIs(self.handlers[0], h0)
|
|
self.assertIs(self.handlers[1], h1)
|
|
self.assertIs(self.handlers[2], h2)
|
|
|