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