Skip to content

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))