tigrbl-tests 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. tests/__init__.py +0 -0
  2. tests/conftest.py +285 -0
  3. tests/i9n/__init__.py +0 -0
  4. tests/i9n/test_acronym_route_name.py +16 -0
  5. tests/i9n/test_allow_anon.py +239 -0
  6. tests/i9n/test_apikey_generation.py +47 -0
  7. tests/i9n/test_authn_provider_integration.py +67 -0
  8. tests/i9n/test_bindings_integration.py +108 -0
  9. tests/i9n/test_bindings_modules.py +149 -0
  10. tests/i9n/test_bulk_docs_client.py +99 -0
  11. tests/i9n/test_core_access.py +164 -0
  12. tests/i9n/test_error_mappings.py +360 -0
  13. tests/i9n/test_field_spec_effects.py +117 -0
  14. tests/i9n/test_header_io_uvicorn.py +71 -0
  15. tests/i9n/test_healthz_methodz_hookz.py +203 -0
  16. tests/i9n/test_hook_ctx_v3_i9n.py +392 -0
  17. tests/i9n/test_hook_lifecycle.py +452 -0
  18. tests/i9n/test_iospec_attributes.py +368 -0
  19. tests/i9n/test_iospec_integration.py +181 -0
  20. tests/i9n/test_key_digest_uvicorn.py +151 -0
  21. tests/i9n/test_list_filters_optional.py +20 -0
  22. tests/i9n/test_mixins.py +534 -0
  23. tests/i9n/test_nested_path_schema_and_rpc.py +34 -0
  24. tests/i9n/test_nested_routing_depth.py +118 -0
  25. tests/i9n/test_op_ctx_alias_examples.py +62 -0
  26. tests/i9n/test_op_ctx_behavior.py +395 -0
  27. tests/i9n/test_op_ctx_core_crud_order.py +219 -0
  28. tests/i9n/test_openapi_clear_response_schema.py +35 -0
  29. tests/i9n/test_openapi_schema_examples_presence.py +81 -0
  30. tests/i9n/test_opspec_effects_i9n_test.py +193 -0
  31. tests/i9n/test_owner_tenant_policy.py +173 -0
  32. tests/i9n/test_request_extras.py +74 -0
  33. tests/i9n/test_request_extras_provider.py +27 -0
  34. tests/i9n/test_response_extras_provider.py +22 -0
  35. tests/i9n/test_rest_fallback_serialization.py +66 -0
  36. tests/i9n/test_rest_row_serialization.py +81 -0
  37. tests/i9n/test_rest_rpc_parity_v3.py +0 -0
  38. tests/i9n/test_row_result_serialization.py +84 -0
  39. tests/i9n/test_schema.py +45 -0
  40. tests/i9n/test_schema_ctx_attributes_integration.py +178 -0
  41. tests/i9n/test_schema_ctx_op_ctx_integration.py +88 -0
  42. tests/i9n/test_schema_ctx_spec_integration.py +209 -0
  43. tests/i9n/test_sqlite_attachments.py +36 -0
  44. tests/i9n/test_storage_spec_integration.py +126 -0
  45. tests/i9n/test_symmetry_parity.py +26 -0
  46. tests/i9n/test_v3_bulk_rest_endpoints.py +120 -0
  47. tests/i9n/test_v3_default_rest_ops.py +145 -0
  48. tests/i9n/test_v3_default_rpc_ops.py +234 -0
  49. tests/i9n/test_v3_opspec_attributes.py +272 -0
  50. tests/i9n/test_verb_alias_policy.py +57 -0
  51. tests/i9n/uvicorn_utils.py +43 -0
  52. tests/perf/__init__.py +0 -0
  53. tests/perf/test_collect_caching.py +42 -0
  54. tests/perf/test_hookz_performance.py +89 -0
  55. tests/perf/test_methodz_performance.py +99 -0
  56. tests/unit/__init__.py +0 -0
  57. tests/unit/decorators/test_alias_ctx_bindings.py +34 -0
  58. tests/unit/decorators/test_engine_ctx_bindings.py +57 -0
  59. tests/unit/decorators/test_hook_ctx_bindings.py +53 -0
  60. tests/unit/decorators/test_op_alias_bindings.py +39 -0
  61. tests/unit/decorators/test_op_ctx_bindings.py +82 -0
  62. tests/unit/decorators/test_response_ctx_bindings.py +32 -0
  63. tests/unit/decorators/test_schema_ctx_bindings.py +39 -0
  64. tests/unit/response_utils.py +142 -0
  65. tests/unit/runtime/atoms/test_emit_paired_post.py +27 -0
  66. tests/unit/runtime/atoms/test_emit_paired_pre.py +38 -0
  67. tests/unit/runtime/atoms/test_emit_readtime_alias.py +41 -0
  68. tests/unit/runtime/atoms/test_out_masking.py +76 -0
  69. tests/unit/runtime/atoms/test_refresh_demand.py +43 -0
  70. tests/unit/runtime/atoms/test_resolve_assemble.py +45 -0
  71. tests/unit/runtime/atoms/test_resolve_paired_gen.py +65 -0
  72. tests/unit/runtime/atoms/test_schema_collect_in.py +44 -0
  73. tests/unit/runtime/atoms/test_schema_collect_out.py +43 -0
  74. tests/unit/runtime/atoms/test_storage_to_stored.py +45 -0
  75. tests/unit/runtime/atoms/test_wire_build_in.py +13 -0
  76. tests/unit/runtime/atoms/test_wire_build_out.py +40 -0
  77. tests/unit/runtime/atoms/test_wire_dump.py +14 -0
  78. tests/unit/runtime/atoms/test_wire_validate_in.py +69 -0
  79. tests/unit/runtime/test_events_phases.py +14 -0
  80. tests/unit/test_acol_vcol_knobs.py +96 -0
  81. tests/unit/test_alias_ctx_op_alias_attributes.py +75 -0
  82. tests/unit/test_alias_ctx_op_attributes.py +61 -0
  83. tests/unit/test_api_level_set_auth.py +25 -0
  84. tests/unit/test_app_model_defaults.py +28 -0
  85. tests/unit/test_app_reexport.py +6 -0
  86. tests/unit/test_base_facade_initialize.py +71 -0
  87. tests/unit/test_build_list_params_spec_model.py +33 -0
  88. tests/unit/test_bulk_body_annotation.py +23 -0
  89. tests/unit/test_bulk_response_schema.py +153 -0
  90. tests/unit/test_colspec_map_isolation.py +19 -0
  91. tests/unit/test_column_collect_mixins.py +21 -0
  92. tests/unit/test_column_rest_rpc_results.py +298 -0
  93. tests/unit/test_column_table_orm_binding.py +51 -0
  94. tests/unit/test_config_dataclass_none.py +12 -0
  95. tests/unit/test_core_crud_bulk_ops.py +160 -0
  96. tests/unit/test_core_crud_default_ops.py +174 -0
  97. tests/unit/test_core_crud_methods.py +337 -0
  98. tests/unit/test_core_wrap_memoization.py +67 -0
  99. tests/unit/test_db_dependency.py +19 -0
  100. tests/unit/test_decorator_and_collect.py +47 -0
  101. tests/unit/test_default_tags.py +20 -0
  102. tests/unit/test_engine_spec_and_shortcuts.py +84 -0
  103. tests/unit/test_engine_usage_levels.py +36 -0
  104. tests/unit/test_field_spec_attrs.py +95 -0
  105. tests/unit/test_file_response.py +148 -0
  106. tests/unit/test_handler_step_qualname.py +30 -0
  107. tests/unit/test_hook_ctx_attributes.py +33 -0
  108. tests/unit/test_hook_ctx_binding.py +55 -0
  109. tests/unit/test_hookz_empty_phase.py +37 -0
  110. tests/unit/test_hybrid_session_run_sync.py +18 -0
  111. tests/unit/test_in_tx.py +16 -0
  112. tests/unit/test_include_models_base_prefix.py +29 -0
  113. tests/unit/test_initialize_cross_ddl.py +26 -0
  114. tests/unit/test_io_spec_attributes.py +171 -0
  115. tests/unit/test_iospec_attributes.py +90 -0
  116. tests/unit/test_iospec_effects.py +173 -0
  117. tests/unit/test_jsonrpc_id_example.py +9 -0
  118. tests/unit/test_jsonrpc_router_default_tag.py +10 -0
  119. tests/unit/test_kernel_invoke_ctx.py +14 -0
  120. tests/unit/test_kernel_opview_on_demand.py +42 -0
  121. tests/unit/test_kernel_plan_labels.py +40 -0
  122. tests/unit/test_kernelz_endpoint.py +65 -0
  123. tests/unit/test_make_column_shortcuts.py +80 -0
  124. tests/unit/test_mixins_sqlalchemy.py +13 -0
  125. tests/unit/test_op_alias.py +70 -0
  126. tests/unit/test_op_class_engine_binding.py +38 -0
  127. tests/unit/test_op_ctx_arity_paths.py +83 -0
  128. tests/unit/test_op_ctx_attributes.py +147 -0
  129. tests/unit/test_op_ctx_core_crud_integration.py +376 -0
  130. tests/unit/test_op_ctx_dynamic_attach.py +19 -0
  131. tests/unit/test_op_ctx_persist_options.py +75 -0
  132. tests/unit/test_opspec_effects.py +153 -0
  133. tests/unit/test_postgres_engine_errors.py +17 -0
  134. tests/unit/test_postgres_env_vars.py +17 -0
  135. tests/unit/test_relationship_alias_cols.py +98 -0
  136. tests/unit/test_request_body_schema.py +54 -0
  137. tests/unit/test_request_response_examples.py +169 -0
  138. tests/unit/test_resolver_precedence.py +49 -0
  139. tests/unit/test_response_alias_table_rpc.py +40 -0
  140. tests/unit/test_response_ctx_precedence.py +62 -0
  141. tests/unit/test_response_diagnostics_kernelz.py +81 -0
  142. tests/unit/test_response_html_jinja_behavior.py +116 -0
  143. tests/unit/test_response_parity.py +20 -0
  144. tests/unit/test_response_rest.py +91 -0
  145. tests/unit/test_response_rpc.py +88 -0
  146. tests/unit/test_response_template.py +30 -0
  147. tests/unit/test_response_uuid.py +58 -0
  148. tests/unit/test_rest_all_default_op_verbs.py +58 -0
  149. tests/unit/test_rest_bulk_delete_suppresses_clear.py +25 -0
  150. tests/unit/test_rest_no_schema_jsonable.py +68 -0
  151. tests/unit/test_rest_operation_id_uniqueness.py +36 -0
  152. tests/unit/test_rest_rpc_parity_default_ops.py +86 -0
  153. tests/unit/test_rest_rpc_prefixes.py +40 -0
  154. tests/unit/test_rest_rpc_symmetry.py +151 -0
  155. tests/unit/test_rpc_all_default_op_verbs.py +224 -0
  156. tests/unit/test_rpc_default_ops.py +111 -0
  157. tests/unit/test_schema_ctx_attributes.py +96 -0
  158. tests/unit/test_schema_ctx_plain_class.py +34 -0
  159. tests/unit/test_schema_spec_presence.py +36 -0
  160. tests/unit/test_schemas_binding.py +29 -0
  161. tests/unit/test_security_per_route.py +43 -0
  162. tests/unit/test_should_wire_canonical.py +62 -0
  163. tests/unit/test_spec_api.py +50 -0
  164. tests/unit/test_spec_app.py +28 -0
  165. tests/unit/test_spec_column.py +24 -0
  166. tests/unit/test_spec_engine.py +61 -0
  167. tests/unit/test_spec_field.py +16 -0
  168. tests/unit/test_spec_hook.py +35 -0
  169. tests/unit/test_spec_io.py +29 -0
  170. tests/unit/test_spec_op.py +31 -0
  171. tests/unit/test_spec_storage.py +21 -0
  172. tests/unit/test_spec_table.py +21 -0
  173. tests/unit/test_sqlite_attachments.py +57 -0
  174. tests/unit/test_storage_spec_attributes.py +78 -0
  175. tests/unit/test_sys_handler_crud.py +86 -0
  176. tests/unit/test_sys_run_rollback.py +42 -0
  177. tests/unit/test_sys_tx_async_begin.py +45 -0
  178. tests/unit/test_sys_tx_begin.py +49 -0
  179. tests/unit/test_sys_tx_commit.py +59 -0
  180. tests/unit/test_table_base_exports.py +22 -0
  181. tests/unit/test_table_collect_spec.py +41 -0
  182. tests/unit/test_table_columns_namespace.py +21 -0
  183. tests/unit/test_v3_favicon_endpoint.py +17 -0
  184. tests/unit/test_v3_healthz_endpoint.py +39 -0
  185. tests/unit/test_v3_op_alias.py +88 -0
  186. tests/unit/test_v3_op_ctx_attributes.py +99 -0
  187. tests/unit/test_v3_schemas_and_decorators.py +114 -0
  188. tests/unit/test_v3_storage_spec_attributes.py +249 -0
  189. tigrbl_tests-0.3.0.dist-info/METADATA +103 -0
  190. tigrbl_tests-0.3.0.dist-info/RECORD +192 -0
  191. tigrbl_tests-0.3.0.dist-info/WHEEL +4 -0
  192. tigrbl_tests-0.3.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,203 @@
1
+ """
2
+ Healthz, Methodz and Hookz Endpoints Tests for Tigrbl v3
3
+
4
+ Tests that healthz, methodz and hookz endpoints are properly attached and behave as expected.
5
+ """
6
+
7
+ import pytest
8
+ from tigrbl import hook_ctx
9
+ from tigrbl.types import SimpleNamespace
10
+
11
+
12
+ @pytest.mark.i9n
13
+ @pytest.mark.asyncio
14
+ async def test_healthz_endpoint_comprehensive(api_client):
15
+ """Test healthz endpoint attachment, behavior, and response format."""
16
+ client, api, _ = api_client
17
+ api.attach_diagnostics(prefix="", app=client._transport.app)
18
+
19
+ # Check that healthz endpoint exists in routes
20
+ routes = [route.path for route in api.router.routes]
21
+ assert "/healthz" in routes
22
+
23
+ # Test healthz response
24
+ response = await client.get("/healthz")
25
+ assert response.status_code == 200
26
+
27
+ # Check content type
28
+ assert response.headers["content-type"].startswith("application/json")
29
+
30
+ # Should return JSON with health status
31
+ data = SimpleNamespace(**response.json())
32
+
33
+ # The actual healthz endpoint returns {'ok': True}
34
+ assert isinstance(data.ok, bool)
35
+ assert data.ok is True
36
+
37
+
38
+ @pytest.mark.i9n
39
+ @pytest.mark.asyncio
40
+ async def test_methodz_endpoint_comprehensive(api_client):
41
+ """Test methodz endpoint attachment, behavior, and response format."""
42
+ client, api, _ = api_client
43
+ api.attach_diagnostics(prefix="", app=client._transport.app)
44
+
45
+ # Check that methodz endpoint exists in routes
46
+ routes = [route.path for route in api.router.routes]
47
+ assert "/methodz" in routes
48
+
49
+ # Test methodz response
50
+ response = await client.get("/methodz")
51
+ assert response.status_code == 200
52
+
53
+ # Check content type
54
+ assert response.headers["content-type"].startswith("application/json")
55
+
56
+ # Should return list of method info dicts
57
+ data = response.json()["methods"]
58
+ assert isinstance(data, list)
59
+
60
+ names = {entry["method"] for entry in data}
61
+
62
+ expected_methods = {
63
+ "Item.create",
64
+ "Item.read",
65
+ "Item.update",
66
+ "Item.delete",
67
+ "Item.list",
68
+ "Tenant.create",
69
+ "Tenant.read",
70
+ "Tenant.update",
71
+ "Tenant.delete",
72
+ "Tenant.list",
73
+ }
74
+
75
+ assert expected_methods.issubset(names)
76
+
77
+
78
+ @pytest.mark.i9n
79
+ @pytest.mark.asyncio
80
+ async def test_hookz_endpoint_comprehensive(api_client):
81
+ """Test hookz endpoint attachment, behavior, and response format."""
82
+ client, api, Item = api_client
83
+
84
+ @hook_ctx(ops="*", phase="POST_RESPONSE")
85
+ def first_hook(cls, ctx):
86
+ pass
87
+
88
+ @hook_ctx(ops="*", phase="POST_RESPONSE")
89
+ def second_hook(cls, ctx):
90
+ pass
91
+
92
+ @hook_ctx(ops="create", phase="POST_RESPONSE")
93
+ def item_hook(cls, ctx):
94
+ pass
95
+
96
+ Item.first_hook = first_hook
97
+ Item.second_hook = second_hook
98
+ Item.item_hook = item_hook
99
+ api.rebind(Item)
100
+ api.attach_diagnostics(prefix="", app=client._transport.app)
101
+
102
+ response = await client.get("/hookz")
103
+ assert response.status_code == 200
104
+ assert response.headers["content-type"].startswith("application/json")
105
+
106
+ data = response.json()
107
+ assert isinstance(data, dict)
108
+
109
+
110
+ @pytest.mark.i9n
111
+ @pytest.mark.asyncio
112
+ async def test_methodz_basic_functionality(api_client):
113
+ """Test that methodz endpoint provides basic method information."""
114
+ client, api, _ = api_client
115
+ api.attach_diagnostics(prefix="", app=client._transport.app)
116
+
117
+ response = await client.get("/methodz")
118
+ methods = {m["method"] for m in response.json()["methods"]}
119
+
120
+ # Should contain Item.create method
121
+ assert "Item.create" in methods
122
+
123
+ # Should contain basic CRUD operations
124
+ crud_operations = ["create", "read", "update", "delete", "list"]
125
+ for operation in crud_operations:
126
+ assert f"Item.{operation}" in methods
127
+ assert f"Tenant.{operation}" in methods
128
+
129
+
130
+ @pytest.mark.i9n
131
+ @pytest.mark.asyncio
132
+ async def test_healthz_methodz_hookz_in_openapi_schema(api_client):
133
+ """Test that healthz, methodz and hookz endpoints are included in OpenAPI schema."""
134
+ client, api, _ = api_client
135
+ api.attach_diagnostics(prefix="", app=client._transport.app)
136
+
137
+ # Get OpenAPI schema
138
+ spec_response = await client.get("/openapi.json")
139
+ spec = spec_response.json()
140
+ paths = spec["paths"]
141
+
142
+ # healthz, methodz and hookz should be in OpenAPI spec
143
+ assert "/healthz" in paths
144
+ assert "/methodz" in paths
145
+ assert "/hookz" in paths
146
+
147
+
148
+ @pytest.mark.i9n
149
+ @pytest.mark.asyncio
150
+ async def test_healthz_database_error_handling(api_client):
151
+ """Test healthz endpoint behavior when database has issues."""
152
+ client, api, _ = api_client
153
+ api.attach_diagnostics(prefix="", app=client._transport.app)
154
+
155
+ # Note: In a real test, we'd mock database connectivity issues
156
+ # For now, we just verify the endpoint responds and has the right structure
157
+ response = await client.get("/healthz")
158
+ assert response.status_code == 200
159
+
160
+ data = SimpleNamespace(**response.json())
161
+ assert isinstance(data.ok, bool)
162
+
163
+ # The actual values depend on database state
164
+ # but structure should always be consistent
165
+
166
+
167
+ @pytest.mark.i9n
168
+ @pytest.mark.asyncio
169
+ async def test_methodz_reflects_dynamic_models(api_client):
170
+ """Test that methodz reflects dynamically registered models."""
171
+ client, api, _ = api_client
172
+ api.attach_diagnostics(prefix="", app=client._transport.app)
173
+
174
+ # Get initial methods
175
+ response = await client.get("/methodz")
176
+ initial_names = {m["method"] for m in response.json()["methods"]}
177
+
178
+ # Should include methods for models from conftest
179
+ for op in ["create", "read", "update", "delete", "list"]:
180
+ assert f"Tenant.{op}" in initial_names
181
+
182
+
183
+ @pytest.mark.i9n
184
+ @pytest.mark.asyncio
185
+ async def test_endpoints_are_synchronous(api_client):
186
+ """Test that healthz, methodz and hookz endpoints work in sync mode."""
187
+ client, api, _ = api_client
188
+ api.attach_diagnostics(prefix="", app=client._transport.app)
189
+
190
+ # These endpoints should work regardless of async/sync context
191
+ healthz_response = await client.get("/healthz")
192
+ assert healthz_response.status_code == 200
193
+
194
+ methodz_response = await client.get("/methodz")
195
+ assert methodz_response.status_code == 200
196
+
197
+ hookz_response = await client.get("/hookz")
198
+ assert hookz_response.status_code == 200
199
+
200
+ # Responses should be immediate and not require async database operations
201
+ assert healthz_response.json()
202
+ assert methodz_response.json()
203
+ assert isinstance(hookz_response.json(), dict)
@@ -0,0 +1,392 @@
1
+ import pytest
2
+ from tigrbl.types import App
3
+ from httpx import ASGITransport, AsyncClient
4
+ from sqlalchemy import func, select
5
+
6
+ from tigrbl import TigrblApp
7
+ from tigrbl.types import Column, String
8
+ from tigrbl.orm.tables import Base
9
+ from tigrbl.orm.mixins import GUIDPk
10
+ from tigrbl.hook import hook_ctx
11
+
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # helpers
15
+ # ---------------------------------------------------------------------------
16
+
17
+
18
+ def create_client(model_cls):
19
+ """Build a FastAPI app with Tigrbl v3 and return an AsyncClient."""
20
+ app = App()
21
+ api = TigrblApp(engine={"kind": "sqlite", "memory": True})
22
+ api.include_model(model_cls)
23
+ api.mount_jsonrpc()
24
+ api.attach_diagnostics()
25
+
26
+ from tigrbl.engine import resolver as _resolver
27
+
28
+ prov = _resolver.resolve_provider(api=api)
29
+ engine, SessionLocal = prov.ensure()
30
+ Base.metadata.create_all(engine)
31
+
32
+ app.include_router(api.router)
33
+ transport = ASGITransport(app=app)
34
+ client = AsyncClient(transport=transport, base_url="http://test")
35
+ return client, api, SessionLocal
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # 0. bindings
40
+ # ---------------------------------------------------------------------------
41
+
42
+
43
+ @pytest.mark.i9n
44
+ @pytest.mark.asyncio
45
+ async def test_hook_ctx_binding_i9n():
46
+ Base.metadata.clear()
47
+ Base.registry.dispose()
48
+
49
+ class Item(Base, GUIDPk):
50
+ __tablename__ = "items"
51
+ name = Column(String, nullable=False)
52
+
53
+ @hook_ctx(ops="create", phase="PRE_HANDLER")
54
+ async def flag(cls, ctx):
55
+ ctx["flagged"] = True
56
+
57
+ client, api, _ = create_client(Item)
58
+ assert any(callable(h) for h in api.hooks.Item.create.PRE_HANDLER)
59
+ await client.aclose()
60
+
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # 1. request and response schemas
64
+ # ---------------------------------------------------------------------------
65
+
66
+
67
+ @pytest.mark.i9n
68
+ @pytest.mark.asyncio
69
+ async def test_hook_ctx_request_response_schema_i9n():
70
+ Base.metadata.clear()
71
+ Base.registry.dispose()
72
+
73
+ class Item(Base, GUIDPk):
74
+ __tablename__ = "items"
75
+ name = Column(String, nullable=False)
76
+
77
+ @hook_ctx(ops="create", phase="POST_RESPONSE")
78
+ async def modify(cls, ctx):
79
+ ctx["response"].result["hook"] = True
80
+
81
+ client, _, _ = create_client(Item)
82
+ res = await client.post("/item", json={"name": "a"})
83
+ assert res.status_code == 201
84
+ assert res.json()["hook"] is True
85
+ await client.aclose()
86
+
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # 2. columns
90
+ # ---------------------------------------------------------------------------
91
+
92
+
93
+ @pytest.mark.i9n
94
+ @pytest.mark.asyncio
95
+ async def test_hook_ctx_columns_i9n():
96
+ Base.metadata.clear()
97
+ Base.registry.dispose()
98
+
99
+ class Item(Base, GUIDPk):
100
+ __tablename__ = "items"
101
+ name = Column(String, nullable=False)
102
+
103
+ @hook_ctx(ops="create", phase="PRE_HANDLER")
104
+ async def collect_cols(cls, ctx):
105
+ ctx["cols"] = list(cls.__table__.columns.keys())
106
+
107
+ @hook_ctx(ops="create", phase="POST_RESPONSE")
108
+ async def expose(cls, ctx):
109
+ ctx["response"].result["cols"] = ctx["cols"]
110
+
111
+ client, _, _ = create_client(Item)
112
+ res = await client.post("/item", json={"name": "x"})
113
+ assert set(res.json()["cols"]) == {"id", "name"}
114
+ await client.aclose()
115
+
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # 3. defaults and value resolution
119
+ # ---------------------------------------------------------------------------
120
+
121
+
122
+ @pytest.mark.i9n
123
+ @pytest.mark.asyncio
124
+ async def test_hook_ctx_defaults_resolution_i9n():
125
+ Base.metadata.clear()
126
+ Base.registry.dispose()
127
+
128
+ class Item(Base, GUIDPk):
129
+ __tablename__ = "items"
130
+ name = Column(String, nullable=True)
131
+
132
+ @hook_ctx(ops="create", phase="PRE_HANDLER")
133
+ async def default_name(cls, ctx):
134
+ ctx.setdefault("payload", {})
135
+ ctx["payload"].setdefault("name", "default")
136
+
137
+ client, _, _ = create_client(Item)
138
+ res = await client.post("/item", json={})
139
+ assert res.status_code == 201
140
+ assert res.json()["name"] == "default"
141
+ await client.aclose()
142
+
143
+
144
+ # ---------------------------------------------------------------------------
145
+ # 4. internal orm models
146
+ # ---------------------------------------------------------------------------
147
+
148
+
149
+ @pytest.mark.i9n
150
+ @pytest.mark.asyncio
151
+ async def test_hook_ctx_internal_model_i9n():
152
+ Base.metadata.clear()
153
+ Base.registry.dispose()
154
+
155
+ class Item(Base, GUIDPk):
156
+ __tablename__ = "items"
157
+ name = Column(String, nullable=False)
158
+
159
+ @hook_ctx(ops="create", phase="PRE_HANDLER")
160
+ async def capture_model(cls, ctx):
161
+ ctx["model_name"] = cls.__name__
162
+
163
+ @hook_ctx(ops="create", phase="POST_RESPONSE")
164
+ async def expose_model(cls, ctx):
165
+ ctx["response"].result["model"] = ctx["model_name"]
166
+
167
+ client, _, _ = create_client(Item)
168
+ res = await client.post("/item", json={"name": "a"})
169
+ assert res.json()["model"] == "Item"
170
+ await client.aclose()
171
+
172
+
173
+ # ---------------------------------------------------------------------------
174
+ # 5. openapi.json
175
+ # ---------------------------------------------------------------------------
176
+
177
+
178
+ @pytest.mark.i9n
179
+ @pytest.mark.asyncio
180
+ async def test_hook_ctx_openapi_json_i9n():
181
+ Base.metadata.clear()
182
+ Base.registry.dispose()
183
+
184
+ class Item(Base, GUIDPk):
185
+ __tablename__ = "items"
186
+ name = Column(String, nullable=False)
187
+
188
+ @hook_ctx(ops="create", phase="PRE_HANDLER")
189
+ async def noop(cls, ctx):
190
+ pass
191
+
192
+ client, _, _ = create_client(Item)
193
+ res = await client.get("/openapi.json")
194
+ assert "/item" in res.json()["paths"]
195
+ await client.aclose()
196
+
197
+
198
+ # ---------------------------------------------------------------------------
199
+ # 6. storage & sqlalchemy
200
+ # ---------------------------------------------------------------------------
201
+
202
+
203
+ @pytest.mark.i9n
204
+ @pytest.mark.asyncio
205
+ async def test_hook_ctx_storage_sqlalchemy_i9n():
206
+ Base.metadata.clear()
207
+ Base.registry.dispose()
208
+
209
+ class Item(Base, GUIDPk):
210
+ __tablename__ = "items"
211
+ name = Column(String, nullable=False)
212
+
213
+ @hook_ctx(ops="create", phase="POST_COMMIT")
214
+ async def count_rows(cls, ctx):
215
+ result = ctx["db"].execute(select(func.count()).select_from(cls)).scalar()
216
+ ctx["count"] = result
217
+
218
+ @hook_ctx(ops="create", phase="POST_RESPONSE")
219
+ async def expose_count(cls, ctx):
220
+ ctx["response"].result["count"] = ctx["count"]
221
+
222
+ client, _, _ = create_client(Item)
223
+ res = await client.post("/item", json={"name": "a"})
224
+ assert res.json()["count"] == 1
225
+ await client.aclose()
226
+
227
+
228
+ # ---------------------------------------------------------------------------
229
+ # 7. rest calls
230
+ # ---------------------------------------------------------------------------
231
+
232
+
233
+ @pytest.mark.i9n
234
+ @pytest.mark.asyncio
235
+ async def test_hook_ctx_rest_call_i9n():
236
+ Base.metadata.clear()
237
+ Base.registry.dispose()
238
+
239
+ class Item(Base, GUIDPk):
240
+ __tablename__ = "items"
241
+ name = Column(String, nullable=False)
242
+
243
+ @hook_ctx(ops="create", phase="POST_RESPONSE")
244
+ async def mark(cls, ctx):
245
+ ctx["response"].result["phase"] = "rest"
246
+
247
+ client, _, _ = create_client(Item)
248
+ res = await client.post("/item", json={"name": "a"})
249
+ assert res.json()["phase"] == "rest"
250
+ await client.aclose()
251
+
252
+
253
+ # ---------------------------------------------------------------------------
254
+ # 8. rpc methods
255
+ # ---------------------------------------------------------------------------
256
+
257
+
258
+ @pytest.mark.i9n
259
+ @pytest.mark.asyncio
260
+ async def test_hook_ctx_rpc_method_i9n():
261
+ Base.metadata.clear()
262
+ Base.registry.dispose()
263
+
264
+ class Item(Base, GUIDPk):
265
+ __tablename__ = "items"
266
+ name = Column(String, nullable=False)
267
+
268
+ @hook_ctx(ops="create", phase="POST_RESPONSE")
269
+ async def mark(cls, ctx):
270
+ ctx["response"].result["phase"] = "rpc"
271
+
272
+ client, _, _ = create_client(Item)
273
+ res = await client.post(
274
+ "/rpc",
275
+ json={
276
+ "jsonrpc": "2.0",
277
+ "id": 1,
278
+ "method": "Item.create",
279
+ "params": {"name": "a"},
280
+ },
281
+ )
282
+ assert res.json()["result"]["phase"] == "rpc"
283
+ await client.aclose()
284
+
285
+
286
+ # ---------------------------------------------------------------------------
287
+ # 9. core.crud
288
+ # ---------------------------------------------------------------------------
289
+
290
+
291
+ @pytest.mark.i9n
292
+ @pytest.mark.asyncio
293
+ async def test_hook_ctx_core_crud_i9n():
294
+ Base.metadata.clear()
295
+ Base.registry.dispose()
296
+
297
+ class Item(Base, GUIDPk):
298
+ __tablename__ = "items"
299
+ name = Column(String, nullable=False)
300
+
301
+ @hook_ctx(ops="create", phase="POST_COMMIT")
302
+ async def mark(cls, ctx):
303
+ ctx["response"].result["via"] = "core"
304
+
305
+ client, api, SessionLocal = create_client(Item)
306
+ with SessionLocal() as session:
307
+ result = await api.core.Item.create({"name": "x"}, db=session)
308
+ assert result["via"] == "core"
309
+ await client.aclose()
310
+
311
+
312
+ # ---------------------------------------------------------------------------
313
+ # 10. hookz
314
+ # ---------------------------------------------------------------------------
315
+
316
+
317
+ @pytest.mark.i9n
318
+ @pytest.mark.asyncio
319
+ async def test_hook_ctx_hookz_i9n():
320
+ Base.metadata.clear()
321
+ Base.registry.dispose()
322
+
323
+ class Item(Base, GUIDPk):
324
+ __tablename__ = "items"
325
+ name = Column(String, nullable=False)
326
+
327
+ @hook_ctx(ops="create", phase="POST_COMMIT")
328
+ async def marker(cls, ctx):
329
+ pass
330
+
331
+ client, _, _ = create_client(Item)
332
+ res = await client.get("/system/hookz")
333
+ data = res.json()
334
+ assert "Item" in data and "create" in data["Item"]
335
+ assert any("marker" in s for s in data["Item"]["create"]["POST_COMMIT"])
336
+ await client.aclose()
337
+
338
+
339
+ # ---------------------------------------------------------------------------
340
+ # 11. atomz
341
+ # ---------------------------------------------------------------------------
342
+
343
+
344
+ @pytest.mark.i9n
345
+ @pytest.mark.asyncio
346
+ async def test_hook_ctx_atomz_i9n():
347
+ Base.metadata.clear()
348
+ Base.registry.dispose()
349
+
350
+ class Item(Base, GUIDPk):
351
+ __tablename__ = "items"
352
+ name = Column(String, nullable=False)
353
+
354
+ @hook_ctx(ops="create", phase="PRE_HANDLER")
355
+ async def capture(cls, ctx):
356
+ ctx["captured"] = ctx["payload"]["name"]
357
+
358
+ @hook_ctx(ops="create", phase="POST_RESPONSE")
359
+ async def expose(cls, ctx):
360
+ ctx["response"].result["captured"] = ctx["captured"]
361
+
362
+ client, _, _ = create_client(Item)
363
+ res = await client.post("/item", json={"name": "alpha"})
364
+ assert res.json()["captured"] == "alpha"
365
+ await client.aclose()
366
+
367
+
368
+ # ---------------------------------------------------------------------------
369
+ # 12. system steps
370
+ # ---------------------------------------------------------------------------
371
+
372
+
373
+ @pytest.mark.i9n
374
+ @pytest.mark.asyncio
375
+ async def test_hook_ctx_system_steps_i9n():
376
+ Base.metadata.clear()
377
+ Base.registry.dispose()
378
+
379
+ class Item(Base, GUIDPk):
380
+ __tablename__ = "items"
381
+ name = Column(String, nullable=False)
382
+
383
+ @hook_ctx(ops="create", phase="POST_COMMIT")
384
+ async def marker(cls, ctx):
385
+ pass
386
+
387
+ client, _, _ = create_client(Item)
388
+ res = await client.get("/system/kernelz")
389
+ data = res.json()
390
+ steps = data["Item"]["create"]
391
+ assert "HANDLER:hook:wire:tigrbl:core:crud:ops:create@HANDLER" in steps
392
+ await client.aclose()