tests/core/test_contract.py¶
Source from this local checkout, regenerated when the reader rebuilds.
Line links use #L<number>; a GitHub line range opens its first line.
1 # Copyright 2025 Softwell S.r.l.2 #3 # Licensed under the Apache License, Version 2.0 (the "License");4 # you may not use this file except in compliance with the License.5 # You may obtain a copy of the License at6 #7 # https://www.apache.org/licenses/LICENSE-2.08 #9 # Unless required by applicable law or agreed to in writing, software10 # distributed under the License is distributed on an "AS IS" BASIS,11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12 # See the License for the specific language governing permissions and13 # limitations under the License.14 15 """Contract tests for the cooperative base classes (SPECIFICATION.md §4, D16).16 17 The cooperative chain: base + two mixin-style layers + concrete class, each18 layer peeling its own kwargs; leftovers raise ``TypeError`` naming them.19 The ownership channel: ``server`` assigned exactly once at attach/mount.20 The base answers: ``authenticate``/``session`` return ``None``.21 """22 23 import asyncio24 25 import pytest26 27 from genro_asgi import BaseApplication, BaseServer28 29 30 class AlphaMixin:31 """Mixin layer: peels ``alpha`` and forwards the rest."""32 33 def __init__(self, **kwargs):34 self.alpha = kwargs.pop("alpha")35 super().__init__(**kwargs)36 37 38 class BetaMixin:39 """Mixin layer: peels ``beta`` and forwards the rest."""40 41 def __init__(self, **kwargs):42 self.beta = kwargs.pop("beta")43 super().__init__(**kwargs)44 45 46 class ConcreteApp(AlphaMixin, BetaMixin, BaseApplication):47 """Cooperative chain: base + two mixin-style layers + concrete class."""48 49 50 class TestCooperativeChain:51 def test_each_layer_receives_its_kwargs(self):52 app = ConcreteApp(alpha=1, beta=2, code="demo")53 assert app.alpha == 154 assert app.beta == 255 assert app.code == "demo"56 57 def test_leftover_kwarg_raises_naming_it(self):58 with pytest.raises(TypeError, match="bogus"):59 ConcreteApp(alpha=1, beta=2, bogus=3)60 61 def test_server_leftover_kwarg_raises_naming_it(self):62 with pytest.raises(TypeError, match="bogus"):63 BaseServer(applications=[BaseApplication(mount="")], bogus=3)64 65 66 class TestOwnershipChannel:67 def test_server_is_none_until_attached(self):68 assert BaseApplication().server is None69 70 def test_registration_assigns_server(self):71 root = BaseApplication(mount="")72 server = BaseServer(applications=[root])73 assert root.server is server74 75 def test_registration_indexes_by_code(self):76 api = BaseApplication(code="api")77 server = BaseServer(applications=[BaseApplication(mount=""), api])78 assert api.server is server79 assert server.applications["api"] is api80 81 def test_second_assignment_raises(self):82 root = BaseApplication(mount="")83 BaseServer(applications=[root])84 with pytest.raises(RuntimeError):85 BaseServer(applications=[root])86 87 def test_serving_the_same_app_on_a_second_server_raises(self):88 api = BaseApplication(code="api")89 BaseServer(applications=[api])90 with pytest.raises(RuntimeError):91 BaseServer(applications=[api])92 93 94 class TestApplicationIdentity:95 def test_code_defaults_to_the_class_name_lowercased(self):96 assert BaseApplication().code == "baseapplication"97 98 def test_mount_defaults_to_the_code(self):99 app = BaseApplication(code="api")100 assert app.mount == "api"101 assert BaseServer(applications=[app]).application_at("api") is app102 103 def test_an_empty_mount_is_the_root_not_a_missing_value(self):104 # ``mount=""`` IS the site root: reading it as "unset" would silently105 # move the app to ``/<code>``.106 root = BaseApplication(code="shop", mount="")107 server = BaseServer(applications=[root])108 assert root.mount == ""109 assert server.root_application is root110 assert server.application_at("") is root111 assert server.application_at("shop") is None112 113 def test_class_attributes_are_the_defaults(self):114 class Fixed(BaseApplication):115 code = "fixed"116 mount = "elsewhere"117 118 assert (Fixed().code, Fixed().mount) == ("fixed", "elsewhere")119 overridden = Fixed(code="other", mount="")120 assert (overridden.code, overridden.mount) == ("other", "")121 122 123 class TestServerContract:124 def test_duplicate_code_raises(self):125 with pytest.raises(ValueError, match="api"):126 BaseServer(applications=[BaseApplication(code="api"), BaseApplication(code="api")])127 128 def test_duplicate_mount_raises(self):129 with pytest.raises(ValueError, match="'api'"):130 BaseServer(131 applications=[132 BaseApplication(code="one", mount="api"),133 BaseApplication(code="two", mount="api"),134 ]135 )136 137 def test_a_server_of_mounts_only_has_no_root_application(self):138 server = BaseServer(applications=[BaseApplication(code="api")])139 assert server.root_application is None140 assert server.default_application is None141 142 def test_a_default_naming_no_served_application_raises(self):143 with pytest.raises(ValueError, match="ghost"):144 BaseServer(applications=[BaseApplication(code="api")], default="ghost")145 146 def test_the_default_is_read_back_as_the_application(self):147 api = BaseApplication(code="api")148 server = BaseServer(applications=[api], default="api")149 assert server.default_application is api150 151 def test_authenticate_answers_nobody(self):152 server = BaseServer(applications=[BaseApplication(mount="")])153 assert server.authenticate(None) is None154 155 def test_session_answers_none(self):156 server = BaseServer(applications=[BaseApplication(mount="")])157 assert server.session(None) is None158 159 160 class TestAppContract:161 def test_lifecycle_hooks_exist_and_default_to_noop(self):162 app = BaseApplication()163 assert app.on_startup() is None164 assert app.on_shutdown() is None165 166 def test_base_asgi_call_is_not_implemented(self):167 app = BaseApplication()168 with pytest.raises(NotImplementedError):169 asyncio.run(app({}, None, None))