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.
- tests/__init__.py +0 -0
- tests/conftest.py +285 -0
- tests/i9n/__init__.py +0 -0
- tests/i9n/test_acronym_route_name.py +16 -0
- tests/i9n/test_allow_anon.py +239 -0
- tests/i9n/test_apikey_generation.py +47 -0
- tests/i9n/test_authn_provider_integration.py +67 -0
- tests/i9n/test_bindings_integration.py +108 -0
- tests/i9n/test_bindings_modules.py +149 -0
- tests/i9n/test_bulk_docs_client.py +99 -0
- tests/i9n/test_core_access.py +164 -0
- tests/i9n/test_error_mappings.py +360 -0
- tests/i9n/test_field_spec_effects.py +117 -0
- tests/i9n/test_header_io_uvicorn.py +71 -0
- tests/i9n/test_healthz_methodz_hookz.py +203 -0
- tests/i9n/test_hook_ctx_v3_i9n.py +392 -0
- tests/i9n/test_hook_lifecycle.py +452 -0
- tests/i9n/test_iospec_attributes.py +368 -0
- tests/i9n/test_iospec_integration.py +181 -0
- tests/i9n/test_key_digest_uvicorn.py +151 -0
- tests/i9n/test_list_filters_optional.py +20 -0
- tests/i9n/test_mixins.py +534 -0
- tests/i9n/test_nested_path_schema_and_rpc.py +34 -0
- tests/i9n/test_nested_routing_depth.py +118 -0
- tests/i9n/test_op_ctx_alias_examples.py +62 -0
- tests/i9n/test_op_ctx_behavior.py +395 -0
- tests/i9n/test_op_ctx_core_crud_order.py +219 -0
- tests/i9n/test_openapi_clear_response_schema.py +35 -0
- tests/i9n/test_openapi_schema_examples_presence.py +81 -0
- tests/i9n/test_opspec_effects_i9n_test.py +193 -0
- tests/i9n/test_owner_tenant_policy.py +173 -0
- tests/i9n/test_request_extras.py +74 -0
- tests/i9n/test_request_extras_provider.py +27 -0
- tests/i9n/test_response_extras_provider.py +22 -0
- tests/i9n/test_rest_fallback_serialization.py +66 -0
- tests/i9n/test_rest_row_serialization.py +81 -0
- tests/i9n/test_rest_rpc_parity_v3.py +0 -0
- tests/i9n/test_row_result_serialization.py +84 -0
- tests/i9n/test_schema.py +45 -0
- tests/i9n/test_schema_ctx_attributes_integration.py +178 -0
- tests/i9n/test_schema_ctx_op_ctx_integration.py +88 -0
- tests/i9n/test_schema_ctx_spec_integration.py +209 -0
- tests/i9n/test_sqlite_attachments.py +36 -0
- tests/i9n/test_storage_spec_integration.py +126 -0
- tests/i9n/test_symmetry_parity.py +26 -0
- tests/i9n/test_v3_bulk_rest_endpoints.py +120 -0
- tests/i9n/test_v3_default_rest_ops.py +145 -0
- tests/i9n/test_v3_default_rpc_ops.py +234 -0
- tests/i9n/test_v3_opspec_attributes.py +272 -0
- tests/i9n/test_verb_alias_policy.py +57 -0
- tests/i9n/uvicorn_utils.py +43 -0
- tests/perf/__init__.py +0 -0
- tests/perf/test_collect_caching.py +42 -0
- tests/perf/test_hookz_performance.py +89 -0
- tests/perf/test_methodz_performance.py +99 -0
- tests/unit/__init__.py +0 -0
- tests/unit/decorators/test_alias_ctx_bindings.py +34 -0
- tests/unit/decorators/test_engine_ctx_bindings.py +57 -0
- tests/unit/decorators/test_hook_ctx_bindings.py +53 -0
- tests/unit/decorators/test_op_alias_bindings.py +39 -0
- tests/unit/decorators/test_op_ctx_bindings.py +82 -0
- tests/unit/decorators/test_response_ctx_bindings.py +32 -0
- tests/unit/decorators/test_schema_ctx_bindings.py +39 -0
- tests/unit/response_utils.py +142 -0
- tests/unit/runtime/atoms/test_emit_paired_post.py +27 -0
- tests/unit/runtime/atoms/test_emit_paired_pre.py +38 -0
- tests/unit/runtime/atoms/test_emit_readtime_alias.py +41 -0
- tests/unit/runtime/atoms/test_out_masking.py +76 -0
- tests/unit/runtime/atoms/test_refresh_demand.py +43 -0
- tests/unit/runtime/atoms/test_resolve_assemble.py +45 -0
- tests/unit/runtime/atoms/test_resolve_paired_gen.py +65 -0
- tests/unit/runtime/atoms/test_schema_collect_in.py +44 -0
- tests/unit/runtime/atoms/test_schema_collect_out.py +43 -0
- tests/unit/runtime/atoms/test_storage_to_stored.py +45 -0
- tests/unit/runtime/atoms/test_wire_build_in.py +13 -0
- tests/unit/runtime/atoms/test_wire_build_out.py +40 -0
- tests/unit/runtime/atoms/test_wire_dump.py +14 -0
- tests/unit/runtime/atoms/test_wire_validate_in.py +69 -0
- tests/unit/runtime/test_events_phases.py +14 -0
- tests/unit/test_acol_vcol_knobs.py +96 -0
- tests/unit/test_alias_ctx_op_alias_attributes.py +75 -0
- tests/unit/test_alias_ctx_op_attributes.py +61 -0
- tests/unit/test_api_level_set_auth.py +25 -0
- tests/unit/test_app_model_defaults.py +28 -0
- tests/unit/test_app_reexport.py +6 -0
- tests/unit/test_base_facade_initialize.py +71 -0
- tests/unit/test_build_list_params_spec_model.py +33 -0
- tests/unit/test_bulk_body_annotation.py +23 -0
- tests/unit/test_bulk_response_schema.py +153 -0
- tests/unit/test_colspec_map_isolation.py +19 -0
- tests/unit/test_column_collect_mixins.py +21 -0
- tests/unit/test_column_rest_rpc_results.py +298 -0
- tests/unit/test_column_table_orm_binding.py +51 -0
- tests/unit/test_config_dataclass_none.py +12 -0
- tests/unit/test_core_crud_bulk_ops.py +160 -0
- tests/unit/test_core_crud_default_ops.py +174 -0
- tests/unit/test_core_crud_methods.py +337 -0
- tests/unit/test_core_wrap_memoization.py +67 -0
- tests/unit/test_db_dependency.py +19 -0
- tests/unit/test_decorator_and_collect.py +47 -0
- tests/unit/test_default_tags.py +20 -0
- tests/unit/test_engine_spec_and_shortcuts.py +84 -0
- tests/unit/test_engine_usage_levels.py +36 -0
- tests/unit/test_field_spec_attrs.py +95 -0
- tests/unit/test_file_response.py +148 -0
- tests/unit/test_handler_step_qualname.py +30 -0
- tests/unit/test_hook_ctx_attributes.py +33 -0
- tests/unit/test_hook_ctx_binding.py +55 -0
- tests/unit/test_hookz_empty_phase.py +37 -0
- tests/unit/test_hybrid_session_run_sync.py +18 -0
- tests/unit/test_in_tx.py +16 -0
- tests/unit/test_include_models_base_prefix.py +29 -0
- tests/unit/test_initialize_cross_ddl.py +26 -0
- tests/unit/test_io_spec_attributes.py +171 -0
- tests/unit/test_iospec_attributes.py +90 -0
- tests/unit/test_iospec_effects.py +173 -0
- tests/unit/test_jsonrpc_id_example.py +9 -0
- tests/unit/test_jsonrpc_router_default_tag.py +10 -0
- tests/unit/test_kernel_invoke_ctx.py +14 -0
- tests/unit/test_kernel_opview_on_demand.py +42 -0
- tests/unit/test_kernel_plan_labels.py +40 -0
- tests/unit/test_kernelz_endpoint.py +65 -0
- tests/unit/test_make_column_shortcuts.py +80 -0
- tests/unit/test_mixins_sqlalchemy.py +13 -0
- tests/unit/test_op_alias.py +70 -0
- tests/unit/test_op_class_engine_binding.py +38 -0
- tests/unit/test_op_ctx_arity_paths.py +83 -0
- tests/unit/test_op_ctx_attributes.py +147 -0
- tests/unit/test_op_ctx_core_crud_integration.py +376 -0
- tests/unit/test_op_ctx_dynamic_attach.py +19 -0
- tests/unit/test_op_ctx_persist_options.py +75 -0
- tests/unit/test_opspec_effects.py +153 -0
- tests/unit/test_postgres_engine_errors.py +17 -0
- tests/unit/test_postgres_env_vars.py +17 -0
- tests/unit/test_relationship_alias_cols.py +98 -0
- tests/unit/test_request_body_schema.py +54 -0
- tests/unit/test_request_response_examples.py +169 -0
- tests/unit/test_resolver_precedence.py +49 -0
- tests/unit/test_response_alias_table_rpc.py +40 -0
- tests/unit/test_response_ctx_precedence.py +62 -0
- tests/unit/test_response_diagnostics_kernelz.py +81 -0
- tests/unit/test_response_html_jinja_behavior.py +116 -0
- tests/unit/test_response_parity.py +20 -0
- tests/unit/test_response_rest.py +91 -0
- tests/unit/test_response_rpc.py +88 -0
- tests/unit/test_response_template.py +30 -0
- tests/unit/test_response_uuid.py +58 -0
- tests/unit/test_rest_all_default_op_verbs.py +58 -0
- tests/unit/test_rest_bulk_delete_suppresses_clear.py +25 -0
- tests/unit/test_rest_no_schema_jsonable.py +68 -0
- tests/unit/test_rest_operation_id_uniqueness.py +36 -0
- tests/unit/test_rest_rpc_parity_default_ops.py +86 -0
- tests/unit/test_rest_rpc_prefixes.py +40 -0
- tests/unit/test_rest_rpc_symmetry.py +151 -0
- tests/unit/test_rpc_all_default_op_verbs.py +224 -0
- tests/unit/test_rpc_default_ops.py +111 -0
- tests/unit/test_schema_ctx_attributes.py +96 -0
- tests/unit/test_schema_ctx_plain_class.py +34 -0
- tests/unit/test_schema_spec_presence.py +36 -0
- tests/unit/test_schemas_binding.py +29 -0
- tests/unit/test_security_per_route.py +43 -0
- tests/unit/test_should_wire_canonical.py +62 -0
- tests/unit/test_spec_api.py +50 -0
- tests/unit/test_spec_app.py +28 -0
- tests/unit/test_spec_column.py +24 -0
- tests/unit/test_spec_engine.py +61 -0
- tests/unit/test_spec_field.py +16 -0
- tests/unit/test_spec_hook.py +35 -0
- tests/unit/test_spec_io.py +29 -0
- tests/unit/test_spec_op.py +31 -0
- tests/unit/test_spec_storage.py +21 -0
- tests/unit/test_spec_table.py +21 -0
- tests/unit/test_sqlite_attachments.py +57 -0
- tests/unit/test_storage_spec_attributes.py +78 -0
- tests/unit/test_sys_handler_crud.py +86 -0
- tests/unit/test_sys_run_rollback.py +42 -0
- tests/unit/test_sys_tx_async_begin.py +45 -0
- tests/unit/test_sys_tx_begin.py +49 -0
- tests/unit/test_sys_tx_commit.py +59 -0
- tests/unit/test_table_base_exports.py +22 -0
- tests/unit/test_table_collect_spec.py +41 -0
- tests/unit/test_table_columns_namespace.py +21 -0
- tests/unit/test_v3_favicon_endpoint.py +17 -0
- tests/unit/test_v3_healthz_endpoint.py +39 -0
- tests/unit/test_v3_op_alias.py +88 -0
- tests/unit/test_v3_op_ctx_attributes.py +99 -0
- tests/unit/test_v3_schemas_and_decorators.py +114 -0
- tests/unit/test_v3_storage_spec_attributes.py +249 -0
- tigrbl_tests-0.3.0.dist-info/METADATA +103 -0
- tigrbl_tests-0.3.0.dist-info/RECORD +192 -0
- tigrbl_tests-0.3.0.dist-info/WHEEL +4 -0
- tigrbl_tests-0.3.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hook Lifecycle Tests for Tigrbl v3
|
|
3
|
+
|
|
4
|
+
Tests all hook phases and their behavior across CRUD, nested CRUD, and RPC operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
from tigrbl import TigrblApp, Base
|
|
10
|
+
from tigrbl.hook import hook_ctx
|
|
11
|
+
from tigrbl.engine.shortcuts import mem
|
|
12
|
+
from tigrbl.orm.mixins import GUIDPk
|
|
13
|
+
from httpx import ASGITransport, AsyncClient
|
|
14
|
+
from sqlalchemy import Column, ForeignKey, String
|
|
15
|
+
from tigrbl.types import PgUUID
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def setup_client(db_mode, Tenant, Item):
|
|
19
|
+
"""Create an Tigrbl client for the provided models."""
|
|
20
|
+
fastapi_app = FastAPI()
|
|
21
|
+
|
|
22
|
+
if db_mode == "async":
|
|
23
|
+
api = TigrblApp(engine=mem())
|
|
24
|
+
api.include_models([Tenant, Item])
|
|
25
|
+
await api.initialize()
|
|
26
|
+
else:
|
|
27
|
+
api = TigrblApp(engine=mem(async_=False))
|
|
28
|
+
api.include_models([Tenant, Item])
|
|
29
|
+
api.initialize()
|
|
30
|
+
|
|
31
|
+
api.mount_jsonrpc()
|
|
32
|
+
fastapi_app.include_router(api.router)
|
|
33
|
+
transport = ASGITransport(app=fastapi_app)
|
|
34
|
+
client = AsyncClient(transport=transport, base_url="http://test")
|
|
35
|
+
return client, api
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.i9n
|
|
39
|
+
@pytest.mark.asyncio
|
|
40
|
+
async def test_hook_phases_execution_order(db_mode):
|
|
41
|
+
"""Test that all hook phases execute in the correct order."""
|
|
42
|
+
|
|
43
|
+
execution_order: list[str] = []
|
|
44
|
+
Base.metadata.clear()
|
|
45
|
+
|
|
46
|
+
class Tenant(Base, GUIDPk):
|
|
47
|
+
__tablename__ = "tenants"
|
|
48
|
+
name = Column(String, nullable=False)
|
|
49
|
+
|
|
50
|
+
class Item(Base, GUIDPk):
|
|
51
|
+
__tablename__ = "items"
|
|
52
|
+
tenant_id = Column(
|
|
53
|
+
PgUUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False
|
|
54
|
+
)
|
|
55
|
+
name = Column(String, nullable=False)
|
|
56
|
+
|
|
57
|
+
@hook_ctx(ops="create", phase="PRE_TX_BEGIN")
|
|
58
|
+
async def pre_tx_begin(cls, ctx):
|
|
59
|
+
execution_order.append("PRE_TX_BEGIN")
|
|
60
|
+
ctx["test_data"] = {"started": True}
|
|
61
|
+
|
|
62
|
+
@hook_ctx(ops="create", phase="PRE_HANDLER")
|
|
63
|
+
async def pre_handler(cls, ctx):
|
|
64
|
+
execution_order.append("PRE_HANDLER")
|
|
65
|
+
assert ctx["test_data"]["started"] is True
|
|
66
|
+
ctx["test_data"]["pre_handler_done"] = True
|
|
67
|
+
|
|
68
|
+
@hook_ctx(ops="create", phase="POST_HANDLER")
|
|
69
|
+
async def post_handler(cls, ctx):
|
|
70
|
+
execution_order.append("POST_HANDLER")
|
|
71
|
+
assert ctx["test_data"]["pre_handler_done"] is True
|
|
72
|
+
ctx["test_data"]["handler_done"] = True
|
|
73
|
+
|
|
74
|
+
@hook_ctx(ops="create", phase="PRE_COMMIT")
|
|
75
|
+
async def pre_commit(cls, ctx):
|
|
76
|
+
execution_order.append("PRE_COMMIT")
|
|
77
|
+
assert ctx["test_data"]["handler_done"] is True
|
|
78
|
+
ctx["test_data"]["pre_commit_done"] = True
|
|
79
|
+
|
|
80
|
+
@hook_ctx(ops="create", phase="POST_COMMIT")
|
|
81
|
+
async def post_commit(cls, ctx):
|
|
82
|
+
execution_order.append("POST_COMMIT")
|
|
83
|
+
assert ctx["test_data"]["pre_commit_done"] is True
|
|
84
|
+
ctx["test_data"]["committed"] = True
|
|
85
|
+
|
|
86
|
+
@hook_ctx(ops="create", phase="POST_RESPONSE")
|
|
87
|
+
async def post_response(cls, ctx):
|
|
88
|
+
execution_order.append("POST_RESPONSE")
|
|
89
|
+
assert ctx["test_data"]["committed"] is True
|
|
90
|
+
ctx["response"].result["hook_completed"] = True
|
|
91
|
+
|
|
92
|
+
client, _ = await setup_client(db_mode, Tenant, Item)
|
|
93
|
+
|
|
94
|
+
t = await client.post("/tenant", json={"name": "test-tenant"})
|
|
95
|
+
tid = t.json()["id"]
|
|
96
|
+
|
|
97
|
+
res = await client.post(
|
|
98
|
+
"/rpc",
|
|
99
|
+
json={
|
|
100
|
+
"method": "Item.create",
|
|
101
|
+
"params": {"tenant_id": tid, "name": "test-item"},
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
assert res.status_code == 200
|
|
106
|
+
data = res.json()["result"]
|
|
107
|
+
assert data["hook_completed"] is True
|
|
108
|
+
|
|
109
|
+
expected_order = [
|
|
110
|
+
"PRE_TX_BEGIN",
|
|
111
|
+
"PRE_HANDLER",
|
|
112
|
+
"POST_HANDLER",
|
|
113
|
+
"PRE_COMMIT",
|
|
114
|
+
"POST_COMMIT",
|
|
115
|
+
"POST_RESPONSE",
|
|
116
|
+
]
|
|
117
|
+
assert execution_order == expected_order
|
|
118
|
+
await client.aclose()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@pytest.mark.i9n
|
|
122
|
+
@pytest.mark.asyncio
|
|
123
|
+
async def test_hook_parity_crud_vs_rpc(db_mode):
|
|
124
|
+
"""Test that hooks execute identically for REST CRUD and RPC calls."""
|
|
125
|
+
|
|
126
|
+
crud_hooks: list[str] = []
|
|
127
|
+
rpc_hooks: list[str] = []
|
|
128
|
+
Base.metadata.clear()
|
|
129
|
+
|
|
130
|
+
class Tenant(Base, GUIDPk):
|
|
131
|
+
__tablename__ = "tenants"
|
|
132
|
+
name = Column(String, nullable=False)
|
|
133
|
+
|
|
134
|
+
class Item(Base, GUIDPk):
|
|
135
|
+
__tablename__ = "items"
|
|
136
|
+
tenant_id = Column(
|
|
137
|
+
PgUUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False
|
|
138
|
+
)
|
|
139
|
+
name = Column(String, nullable=False)
|
|
140
|
+
|
|
141
|
+
@hook_ctx(ops="create", phase="PRE_TX_BEGIN")
|
|
142
|
+
async def track_pre_tx(cls, ctx):
|
|
143
|
+
if hasattr(ctx.get("request"), "url") and "/rpc" in str(ctx["request"].url):
|
|
144
|
+
rpc_hooks.append("PRE_TX_BEGIN")
|
|
145
|
+
else:
|
|
146
|
+
crud_hooks.append("PRE_TX_BEGIN")
|
|
147
|
+
|
|
148
|
+
@hook_ctx(ops="create", phase="POST_COMMIT")
|
|
149
|
+
async def track_post_commit(cls, ctx):
|
|
150
|
+
if hasattr(ctx.get("request"), "url") and "/rpc" in str(ctx["request"].url):
|
|
151
|
+
rpc_hooks.append("POST_COMMIT")
|
|
152
|
+
else:
|
|
153
|
+
crud_hooks.append("POST_COMMIT")
|
|
154
|
+
|
|
155
|
+
client, _ = await setup_client(db_mode, Tenant, Item)
|
|
156
|
+
|
|
157
|
+
t = await client.post("/tenant", json={"name": "test-tenant"})
|
|
158
|
+
tid = t.json()["id"]
|
|
159
|
+
|
|
160
|
+
await client.post("/item", json={"tenant_id": tid, "name": "crud-item"})
|
|
161
|
+
|
|
162
|
+
await client.post(
|
|
163
|
+
"/rpc",
|
|
164
|
+
json={
|
|
165
|
+
"method": "Item.create",
|
|
166
|
+
"params": {"tenant_id": tid, "name": "rpc-item"},
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
assert crud_hooks == ["PRE_TX_BEGIN", "POST_COMMIT"]
|
|
171
|
+
assert rpc_hooks == ["PRE_TX_BEGIN", "POST_COMMIT"]
|
|
172
|
+
await client.aclose()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@pytest.mark.i9n
|
|
176
|
+
@pytest.mark.asyncio
|
|
177
|
+
async def test_hook_error_handling(db_mode):
|
|
178
|
+
"""Test hook behavior during error conditions."""
|
|
179
|
+
|
|
180
|
+
error_hooks: list[str] = []
|
|
181
|
+
Base.metadata.clear()
|
|
182
|
+
|
|
183
|
+
class Tenant(Base, GUIDPk):
|
|
184
|
+
__tablename__ = "tenants"
|
|
185
|
+
name = Column(String, nullable=False)
|
|
186
|
+
|
|
187
|
+
class Item(Base, GUIDPk):
|
|
188
|
+
__tablename__ = "items"
|
|
189
|
+
tenant_id = Column(
|
|
190
|
+
PgUUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False
|
|
191
|
+
)
|
|
192
|
+
name = Column(String, nullable=False)
|
|
193
|
+
|
|
194
|
+
@hook_ctx(ops="*", phase="ON_ERROR")
|
|
195
|
+
async def error_handler(cls, ctx):
|
|
196
|
+
error_hooks.append("ERROR_HANDLED")
|
|
197
|
+
ctx["error_data"] = {"handled": True}
|
|
198
|
+
|
|
199
|
+
@hook_ctx(ops="create", phase="PRE_TX_BEGIN")
|
|
200
|
+
async def failing_hook(cls, ctx):
|
|
201
|
+
raise ValueError("Intentional test error")
|
|
202
|
+
|
|
203
|
+
client, _ = await setup_client(db_mode, Tenant, Item)
|
|
204
|
+
|
|
205
|
+
t = await client.post("/tenant", json={"name": "test-tenant"})
|
|
206
|
+
tid = t.json()["id"]
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
res = await client.post(
|
|
210
|
+
"/item",
|
|
211
|
+
json={"tenant_id": tid, "name": "error-item"},
|
|
212
|
+
)
|
|
213
|
+
assert res.status_code >= 400
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
assert error_hooks == ["ERROR_HANDLED"]
|
|
218
|
+
await client.aclose()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@pytest.mark.i9n
|
|
222
|
+
@pytest.mark.asyncio
|
|
223
|
+
async def test_hook_early_termination_and_cleanup(db_mode):
|
|
224
|
+
"""Test early termination when a hook raises and ensure cleanup."""
|
|
225
|
+
|
|
226
|
+
execution_order: list[str] = []
|
|
227
|
+
Base.metadata.clear()
|
|
228
|
+
|
|
229
|
+
class Tenant(Base, GUIDPk):
|
|
230
|
+
__tablename__ = "tenants"
|
|
231
|
+
name = Column(String, nullable=False)
|
|
232
|
+
|
|
233
|
+
class Item(Base, GUIDPk):
|
|
234
|
+
__tablename__ = "items"
|
|
235
|
+
tenant_id = Column(
|
|
236
|
+
PgUUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False
|
|
237
|
+
)
|
|
238
|
+
name = Column(String, nullable=False)
|
|
239
|
+
|
|
240
|
+
@hook_ctx(ops="create", phase="PRE_TX_BEGIN")
|
|
241
|
+
async def pre_tx_begin(cls, ctx):
|
|
242
|
+
execution_order.append("PRE_TX_BEGIN")
|
|
243
|
+
|
|
244
|
+
@hook_ctx(ops="create", phase="PRE_HANDLER")
|
|
245
|
+
async def pre_handler(cls, ctx):
|
|
246
|
+
execution_order.append("PRE_HANDLER")
|
|
247
|
+
|
|
248
|
+
@hook_ctx(ops="create", phase="POST_HANDLER")
|
|
249
|
+
async def post_handler(cls, ctx):
|
|
250
|
+
execution_order.append("POST_HANDLER")
|
|
251
|
+
|
|
252
|
+
@hook_ctx(ops="create", phase="PRE_COMMIT")
|
|
253
|
+
async def pre_commit(cls, ctx):
|
|
254
|
+
execution_order.append("PRE_COMMIT")
|
|
255
|
+
raise RuntimeError("boom")
|
|
256
|
+
|
|
257
|
+
@hook_ctx(ops="create", phase="POST_COMMIT")
|
|
258
|
+
async def post_commit(cls, ctx):
|
|
259
|
+
execution_order.append("POST_COMMIT")
|
|
260
|
+
|
|
261
|
+
@hook_ctx(ops="create", phase="POST_RESPONSE")
|
|
262
|
+
async def post_response(cls, ctx):
|
|
263
|
+
execution_order.append("POST_RESPONSE")
|
|
264
|
+
|
|
265
|
+
client, _ = await setup_client(db_mode, Tenant, Item)
|
|
266
|
+
|
|
267
|
+
t = await client.post("/tenant", json={"name": "test-tenant"})
|
|
268
|
+
tid = t.json()["id"]
|
|
269
|
+
|
|
270
|
+
before = await client.get("/item")
|
|
271
|
+
assert before.json() == []
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
res = await client.post(
|
|
275
|
+
"/item",
|
|
276
|
+
json={"tenant_id": tid, "name": "fail-item"},
|
|
277
|
+
)
|
|
278
|
+
assert res.status_code >= 400
|
|
279
|
+
except RuntimeError:
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
after = await client.get("/item")
|
|
283
|
+
assert after.json() == []
|
|
284
|
+
|
|
285
|
+
assert execution_order == [
|
|
286
|
+
"PRE_TX_BEGIN",
|
|
287
|
+
"PRE_HANDLER",
|
|
288
|
+
"POST_HANDLER",
|
|
289
|
+
"PRE_COMMIT",
|
|
290
|
+
]
|
|
291
|
+
await client.aclose()
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@pytest.mark.i9n
|
|
295
|
+
@pytest.mark.asyncio
|
|
296
|
+
async def test_hook_context_modification(db_mode):
|
|
297
|
+
"""Test that hooks can modify context and affect subsequent hooks."""
|
|
298
|
+
|
|
299
|
+
hook_executions: list[str] = []
|
|
300
|
+
Base.metadata.clear()
|
|
301
|
+
|
|
302
|
+
class Tenant(Base, GUIDPk):
|
|
303
|
+
__tablename__ = "tenants"
|
|
304
|
+
name = Column(String, nullable=False)
|
|
305
|
+
|
|
306
|
+
class Item(Base, GUIDPk):
|
|
307
|
+
__tablename__ = "items"
|
|
308
|
+
tenant_id = Column(
|
|
309
|
+
PgUUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False
|
|
310
|
+
)
|
|
311
|
+
name = Column(String, nullable=False)
|
|
312
|
+
|
|
313
|
+
@hook_ctx(ops="create", phase="PRE_TX_BEGIN")
|
|
314
|
+
async def modify_params(cls, ctx):
|
|
315
|
+
hook_executions.append("PRE_TX_BEGIN")
|
|
316
|
+
ctx["custom_data"] = {"modified": True}
|
|
317
|
+
|
|
318
|
+
@hook_ctx(ops="create", phase="POST_HANDLER")
|
|
319
|
+
async def verify_modification(cls, ctx):
|
|
320
|
+
hook_executions.append("POST_HANDLER")
|
|
321
|
+
assert ctx["custom_data"]["modified"] is True
|
|
322
|
+
ctx["custom_data"]["verified"] = True
|
|
323
|
+
|
|
324
|
+
@hook_ctx(ops="create", phase="POST_RESPONSE")
|
|
325
|
+
async def enrich_response(cls, ctx):
|
|
326
|
+
hook_executions.append("POST_RESPONSE")
|
|
327
|
+
assert ctx["custom_data"]["verified"] is True
|
|
328
|
+
assert hasattr(ctx["response"].result, "name")
|
|
329
|
+
|
|
330
|
+
client, _ = await setup_client(db_mode, Tenant, Item)
|
|
331
|
+
|
|
332
|
+
t = await client.post("/tenant", json={"name": "test-tenant"})
|
|
333
|
+
tid = t.json()["id"]
|
|
334
|
+
|
|
335
|
+
res = await client.post("/item", json={"tenant_id": tid, "name": "test-item"})
|
|
336
|
+
assert res.status_code == 201
|
|
337
|
+
data = res.json()
|
|
338
|
+
assert data["name"] == "test-item"
|
|
339
|
+
|
|
340
|
+
assert hook_executions == ["PRE_TX_BEGIN", "POST_HANDLER", "POST_RESPONSE"]
|
|
341
|
+
await client.aclose()
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@pytest.mark.i9n
|
|
345
|
+
@pytest.mark.asyncio
|
|
346
|
+
async def test_catch_all_hooks(db_mode):
|
|
347
|
+
"""Test that catch-all hooks (no model/op specified) work correctly."""
|
|
348
|
+
|
|
349
|
+
catch_all_executions: list[str] = []
|
|
350
|
+
Base.metadata.clear()
|
|
351
|
+
|
|
352
|
+
class CatchAllMixin:
|
|
353
|
+
@hook_ctx(ops="*", phase="POST_COMMIT")
|
|
354
|
+
async def catch_all_hook(cls, ctx):
|
|
355
|
+
method = f"{cls.__name__}.{getattr(ctx.get('env'), 'method', 'unknown')}"
|
|
356
|
+
catch_all_executions.append(method)
|
|
357
|
+
|
|
358
|
+
@hook_ctx(ops="*", phase="POST_HANDLER")
|
|
359
|
+
async def post_handler_hook(cls, ctx):
|
|
360
|
+
method = f"{cls.__name__}.{getattr(ctx.get('env'), 'method', 'unknown')}"
|
|
361
|
+
if method.endswith(".delete") and method not in catch_all_executions:
|
|
362
|
+
catch_all_executions.append(method)
|
|
363
|
+
|
|
364
|
+
class Tenant(CatchAllMixin, Base, GUIDPk):
|
|
365
|
+
__tablename__ = "tenants"
|
|
366
|
+
name = Column(String, nullable=False)
|
|
367
|
+
|
|
368
|
+
class Item(CatchAllMixin, Base, GUIDPk):
|
|
369
|
+
__tablename__ = "items"
|
|
370
|
+
tenant_id = Column(
|
|
371
|
+
PgUUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False
|
|
372
|
+
)
|
|
373
|
+
name = Column(String, nullable=False)
|
|
374
|
+
|
|
375
|
+
client, _ = await setup_client(db_mode, Tenant, Item)
|
|
376
|
+
|
|
377
|
+
t = await client.post("/tenant", json={"name": "test-tenant"})
|
|
378
|
+
tid = t.json()["id"]
|
|
379
|
+
|
|
380
|
+
await client.post("/item", json={"tenant_id": tid, "name": "test-item"})
|
|
381
|
+
|
|
382
|
+
items = await client.get("/item")
|
|
383
|
+
item_id = items.json()[0]["id"]
|
|
384
|
+
await client.get(f"/item/{item_id}")
|
|
385
|
+
|
|
386
|
+
update_res = await client.patch(
|
|
387
|
+
f"/item/{item_id}", json={"tenant_id": tid, "name": "updated-item"}
|
|
388
|
+
)
|
|
389
|
+
update_succeeded = update_res.status_code < 400
|
|
390
|
+
|
|
391
|
+
delete_res = await client.delete(f"/item/{item_id}")
|
|
392
|
+
delete_succeeded = delete_res.status_code < 400
|
|
393
|
+
|
|
394
|
+
expected_methods = [
|
|
395
|
+
"Tenant.create",
|
|
396
|
+
"Item.create",
|
|
397
|
+
"Item.list",
|
|
398
|
+
"Item.read",
|
|
399
|
+
]
|
|
400
|
+
if update_succeeded:
|
|
401
|
+
expected_methods.append("Item.update")
|
|
402
|
+
if delete_succeeded:
|
|
403
|
+
expected_methods.append("Item.delete")
|
|
404
|
+
|
|
405
|
+
unique_methods = list(dict.fromkeys(catch_all_executions))
|
|
406
|
+
assert unique_methods == expected_methods
|
|
407
|
+
await client.aclose()
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@pytest.mark.i9n
|
|
411
|
+
@pytest.mark.asyncio
|
|
412
|
+
async def test_multiple_hooks_same_phase(db_mode):
|
|
413
|
+
"""Test that multiple hooks for the same phase execute correctly."""
|
|
414
|
+
|
|
415
|
+
executions: list[str] = []
|
|
416
|
+
Base.metadata.clear()
|
|
417
|
+
|
|
418
|
+
class Tenant(Base, GUIDPk):
|
|
419
|
+
__tablename__ = "tenants"
|
|
420
|
+
name = Column(String, nullable=False)
|
|
421
|
+
|
|
422
|
+
class Item(Base, GUIDPk):
|
|
423
|
+
__tablename__ = "items"
|
|
424
|
+
tenant_id = Column(
|
|
425
|
+
PgUUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False
|
|
426
|
+
)
|
|
427
|
+
name = Column(String, nullable=False)
|
|
428
|
+
|
|
429
|
+
@hook_ctx(ops="create", phase="POST_COMMIT")
|
|
430
|
+
async def first_hook(cls, ctx):
|
|
431
|
+
executions.append("first")
|
|
432
|
+
|
|
433
|
+
@hook_ctx(ops="create", phase="POST_COMMIT")
|
|
434
|
+
async def second_hook(cls, ctx):
|
|
435
|
+
executions.append("second")
|
|
436
|
+
|
|
437
|
+
@hook_ctx(ops="create", phase="POST_COMMIT")
|
|
438
|
+
async def third_hook(cls, ctx):
|
|
439
|
+
executions.append("third")
|
|
440
|
+
|
|
441
|
+
client, _ = await setup_client(db_mode, Tenant, Item)
|
|
442
|
+
|
|
443
|
+
t = await client.post("/tenant", json={"name": "test-tenant"})
|
|
444
|
+
tid = t.json()["id"]
|
|
445
|
+
|
|
446
|
+
await client.post("/item", json={"tenant_id": tid, "name": "test-item"})
|
|
447
|
+
|
|
448
|
+
assert len(executions) == 3
|
|
449
|
+
assert "first" in executions
|
|
450
|
+
assert "second" in executions
|
|
451
|
+
assert "third" in executions
|
|
452
|
+
await client.aclose()
|