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,368 @@
1
+ import pytest
2
+ from tigrbl.types import App, SimpleNamespace
3
+ from fastapi.testclient import TestClient
4
+ from sqlalchemy import create_engine
5
+ from sqlalchemy.orm import Mapped, sessionmaker
6
+ from sqlalchemy.pool import StaticPool
7
+
8
+ from tigrbl import TigrblApp
9
+ from tigrbl.engine.shortcuts import engine as engine_factory, mem
10
+ from tigrbl.bindings.model import bind
11
+ from tigrbl.bindings.rest.router import _build_router
12
+ from tigrbl.bindings.rpc import register_and_attach
13
+ from tigrbl.op import OpSpec
14
+ from tigrbl.runtime.atoms.resolve import assemble
15
+ from tigrbl.runtime.atoms.schema import collect_in, collect_out
16
+ from tigrbl.runtime.kernel import _default_kernel as K, build_phase_chains
17
+ from tigrbl.specs import F, IO, S, acol, vcol
18
+ from tigrbl.orm.tables import Base
19
+ from tigrbl.orm.mixins import GUIDPk
20
+ from tigrbl.types import Integer as IntType, String as StrType
21
+
22
+
23
+ @pytest.mark.i9n
24
+ def test_request_and_response_schemas_respect_iospec_aliases():
25
+ class Thing(Base):
26
+ __tablename__ = "iospec_schema_i9n"
27
+ __allow_unmapped__ = True
28
+
29
+ id = acol(
30
+ storage=S(type_=IntType, primary_key=True, autoincrement=True),
31
+ io=IO(out_verbs=("read",)),
32
+ )
33
+ name = acol(
34
+ storage=S(type_=StrType, nullable=False),
35
+ io=IO(
36
+ in_verbs=("create",),
37
+ out_verbs=("read",),
38
+ alias_in="first_name",
39
+ alias_out="firstName",
40
+ ),
41
+ )
42
+
43
+ bind(Thing)
44
+ specs = Thing.__tigrbl_cols__
45
+ ctx_in = SimpleNamespace(
46
+ opview=K._compile_opview_from_specs(specs, SimpleNamespace(alias="create")),
47
+ op="create",
48
+ temp={},
49
+ )
50
+ collect_in.run(None, ctx_in)
51
+ schema_in = ctx_in.temp["schema_in"]
52
+ assert "id" not in schema_in["by_field"]
53
+ assert schema_in["by_field"]["name"]["alias_in"] == "first_name"
54
+
55
+ ctx_out = SimpleNamespace(
56
+ opview=K._compile_opview_from_specs(specs, SimpleNamespace(alias="read")),
57
+ op="read",
58
+ temp={},
59
+ )
60
+ collect_out.run(None, ctx_out)
61
+ schema_out = ctx_out.temp["schema_out"]
62
+ assert "id" in schema_out["by_field"]
63
+ assert schema_out["by_field"]["name"]["alias_out"] == "firstName"
64
+
65
+
66
+ @pytest.mark.i9n
67
+ def test_columns_materialized_for_acol():
68
+ class Thing(Base):
69
+ __tablename__ = "iospec_columns_i9n"
70
+ __allow_unmapped__ = True
71
+
72
+ id = acol(
73
+ storage=S(type_=IntType, primary_key=True), io=IO(out_verbs=("read",))
74
+ )
75
+ nick: Mapped[str] = vcol(field=F(py_type=str), io=IO(out_verbs=("read",)))
76
+
77
+ bind(Thing)
78
+ assert "id" in Thing.__table__.c
79
+ assert "nick" in Thing.__table__.c
80
+
81
+
82
+ @pytest.mark.i9n
83
+ def test_default_factory_resolves_missing_value():
84
+ class Thing(Base):
85
+ __tablename__ = "iospec_defaults_i9n"
86
+ __allow_unmapped__ = True
87
+
88
+ id = acol(
89
+ storage=S(type_=IntType, primary_key=True, autoincrement=True),
90
+ io=IO(out_verbs=("read",)),
91
+ )
92
+ created = acol(
93
+ storage=S(type_=StrType, nullable=False),
94
+ io=IO(in_verbs=("create",)),
95
+ default_factory=lambda ctx: "now",
96
+ )
97
+
98
+ bind(Thing)
99
+ specs = Thing.__tigrbl_cols__
100
+ ctx = SimpleNamespace(
101
+ opview=K._compile_opview_from_specs(specs, SimpleNamespace(alias="create")),
102
+ op="create",
103
+ temp={"in_values": {}},
104
+ persist=True,
105
+ )
106
+ assemble.run(None, ctx)
107
+ assembled = ctx.temp["assembled_values"]
108
+ assert assembled["created"] == "now"
109
+ assert "created" in ctx.temp["used_default_factory"]
110
+
111
+
112
+ @pytest.mark.i9n
113
+ def test_binding_attaches_internal_model_namespaces():
114
+ class Thing(Base):
115
+ __tablename__ = "iospec_internal_i9n"
116
+ __allow_unmapped__ = True
117
+
118
+ id = acol(
119
+ storage=S(type_=IntType, primary_key=True), io=IO(out_verbs=("read",))
120
+ )
121
+ name = acol(
122
+ storage=S(type_=StrType, nullable=False),
123
+ io=IO(in_verbs=("create",), out_verbs=("read",)),
124
+ )
125
+
126
+ api = TigrblApp()
127
+ api.include_model(Thing, mount_router=False)
128
+ assert "Thing" in api.models
129
+ assert hasattr(api.schemas, "Thing")
130
+ assert "name" in Thing.__tigrbl_cols__
131
+
132
+
133
+ @pytest.mark.i9n
134
+ def test_openapi_reflects_io_verbs():
135
+ class Widget(Base, GUIDPk):
136
+ __tablename__ = "iospec_openapi_i9n"
137
+ __allow_unmapped__ = True
138
+
139
+ name = acol(
140
+ storage=S(type_=StrType, nullable=False),
141
+ io=IO(in_verbs=("create",), out_verbs=("read",)),
142
+ )
143
+
144
+ sp_create = OpSpec(alias="create", target="create")
145
+ sp_read = OpSpec(alias="read", target="read")
146
+ router = _build_router(Widget, [sp_create, sp_read])
147
+ app = App()
148
+ app.include_router(router)
149
+ spec = app.openapi()
150
+
151
+ props_create = spec["components"]["schemas"]["WidgetCreateRequest"]["properties"]
152
+ assert "name" in props_create
153
+ assert "id" not in props_create
154
+
155
+ props_read = spec["components"]["schemas"]["WidgetReadResponse"]["properties"]
156
+ assert "name" in props_read
157
+ assert "id" in props_read
158
+
159
+
160
+ @pytest.mark.i9n
161
+ def test_storage_and_sqlalchemy_integration():
162
+ engine = create_engine(
163
+ "sqlite:///:memory:",
164
+ connect_args={"check_same_thread": False},
165
+ poolclass=StaticPool,
166
+ )
167
+ SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)
168
+ Base.metadata.clear()
169
+
170
+ class Thing(Base):
171
+ __tablename__ = "iospec_storage_i9n"
172
+ __allow_unmapped__ = True
173
+
174
+ id = acol(
175
+ storage=S(type_=IntType, primary_key=True, autoincrement=True),
176
+ io=IO(out_verbs=("read",)),
177
+ )
178
+ name = acol(
179
+ storage=S(type_=StrType, nullable=False),
180
+ io=IO(in_verbs=("create",), out_verbs=("read",)),
181
+ )
182
+
183
+ bind(Thing)
184
+ Base.metadata.create_all(engine)
185
+
186
+ with SessionLocal() as session:
187
+ obj = Thing(name="foo")
188
+ session.add(obj)
189
+ session.commit()
190
+ session.refresh(obj)
191
+ assert obj.id is not None
192
+ stored = session.get(Thing, obj.id)
193
+ assert stored.name == "foo"
194
+
195
+
196
+ @pytest.mark.i9n
197
+ def test_rest_call_respects_aliases():
198
+ eng = engine_factory(mem(async_=False))
199
+
200
+ class Thing(Base):
201
+ __tablename__ = "iospec_rest_i9n"
202
+ __allow_unmapped__ = True
203
+
204
+ id = acol(
205
+ storage=S(type_=IntType, primary_key=True, autoincrement=True),
206
+ io=IO(out_verbs=("read",)),
207
+ )
208
+ name = acol(
209
+ storage=S(type_=StrType, nullable=False),
210
+ io=IO(in_verbs=("create",), out_verbs=("read",)),
211
+ )
212
+
213
+ api = TigrblApp(engine=eng)
214
+ api.include_model(Thing)
215
+ Base.metadata.create_all(eng.raw()[0])
216
+ client = TestClient(api)
217
+
218
+ resp = client.post("/thing", json={"name": "Ada"})
219
+ data = resp.json()
220
+ assert data["name"] == "Ada"
221
+
222
+
223
+ @pytest.mark.i9n
224
+ @pytest.mark.asyncio
225
+ async def test_rpc_call_uses_schemas():
226
+ engine = create_engine(
227
+ "sqlite:///:memory:",
228
+ connect_args={"check_same_thread": False},
229
+ poolclass=StaticPool,
230
+ )
231
+ SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)
232
+ Base.metadata.clear()
233
+
234
+ class Thing(Base):
235
+ __tablename__ = "iospec_rpc_i9n"
236
+ __allow_unmapped__ = True
237
+
238
+ id = acol(
239
+ storage=S(type_=IntType, primary_key=True, autoincrement=True),
240
+ io=IO(out_verbs=("read",)),
241
+ )
242
+ name = acol(
243
+ storage=S(type_=StrType, nullable=False),
244
+ io=IO(in_verbs=("create",), out_verbs=("read",)),
245
+ )
246
+
247
+ bind(Thing)
248
+ register_and_attach(Thing, [OpSpec(alias="create", target="create")])
249
+ Base.metadata.create_all(engine)
250
+
251
+ with SessionLocal() as session:
252
+ result = await Thing.rpc.create({"name": "Bob"}, db=session)
253
+ assert result["name"] == "Bob"
254
+
255
+
256
+ @pytest.mark.i9n
257
+ @pytest.mark.asyncio
258
+ async def test_core_crud_helpers_operate():
259
+ eng = engine_factory(mem(async_=False))
260
+
261
+ class Thing(Base):
262
+ __tablename__ = "iospec_core_i9n"
263
+ __allow_unmapped__ = True
264
+
265
+ id = acol(
266
+ storage=S(type_=IntType, primary_key=True, autoincrement=True),
267
+ io=IO(out_verbs=("read",)),
268
+ )
269
+ name = acol(
270
+ storage=S(type_=StrType, nullable=False),
271
+ io=IO(in_verbs=("create",), out_verbs=("read",)),
272
+ )
273
+
274
+ api = TigrblApp(engine=eng)
275
+ api.include_model(Thing)
276
+ Base.metadata.create_all(eng.raw()[0])
277
+
278
+ with eng.session() as session:
279
+ created = await api.core.Thing.create({"name": "Zed"}, db=session)
280
+ obj = await api.core.Thing.read({"id": created["id"]}, db=session)
281
+ assert obj["name"] == "Zed"
282
+
283
+
284
+ @pytest.mark.i9n
285
+ @pytest.mark.asyncio
286
+ async def test_hooks_trigger_with_iospec():
287
+ engine = create_engine(
288
+ "sqlite:///:memory:",
289
+ connect_args={"check_same_thread": False},
290
+ poolclass=StaticPool,
291
+ )
292
+ SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)
293
+
294
+ called = {}
295
+
296
+ async def before(ctx):
297
+ called["hit"] = True
298
+
299
+ class Thing(Base):
300
+ __tablename__ = "iospec_hooks_i9n"
301
+ __allow_unmapped__ = True
302
+
303
+ id = acol(
304
+ storage=S(type_=IntType, primary_key=True, autoincrement=True),
305
+ io=IO(out_verbs=("read",)),
306
+ )
307
+ name = acol(
308
+ storage=S(type_=StrType, nullable=False),
309
+ io=IO(in_verbs=("create",), out_verbs=("read",)),
310
+ )
311
+ __tigrbl_hooks__ = {"create": {"PRE_HANDLER": [before]}}
312
+
313
+ bind(Thing)
314
+ register_and_attach(Thing, [OpSpec(alias="create", target="create")])
315
+ Base.metadata.create_all(engine)
316
+
317
+ with SessionLocal() as session:
318
+ await Thing.rpc.create({"name": "hi"}, db=session)
319
+ assert called.get("hit") is True
320
+
321
+
322
+ @pytest.mark.i9n
323
+ def test_atoms_execute_with_iospec():
324
+ class Thing(Base):
325
+ __tablename__ = "iospec_atoms_i9n"
326
+ __allow_unmapped__ = True
327
+
328
+ id = acol(
329
+ storage=S(type_=IntType, primary_key=True, autoincrement=True),
330
+ io=IO(out_verbs=("read",)),
331
+ )
332
+ name = acol(
333
+ storage=S(type_=StrType, nullable=False),
334
+ io=IO(in_verbs=("create",), out_verbs=("read",)),
335
+ )
336
+
337
+ bind(Thing)
338
+ specs = Thing.__tigrbl_cols__
339
+ ctx = SimpleNamespace(
340
+ opview=K._compile_opview_from_specs(specs, SimpleNamespace(alias="create")),
341
+ op="create",
342
+ temp={"in_values": {"name": "x"}},
343
+ persist=True,
344
+ )
345
+ collect_in.run(None, ctx)
346
+ assemble.run(None, ctx)
347
+ collect_out.run(None, ctx)
348
+ assert ctx.temp["assembled_values"]["name"] == "x"
349
+
350
+
351
+ @pytest.mark.i9n
352
+ def test_system_phase_chain_includes_system_steps():
353
+ class Thing(Base):
354
+ __tablename__ = "iospec_system_i9n"
355
+ __allow_unmapped__ = True
356
+
357
+ id = acol(
358
+ storage=S(type_=IntType, primary_key=True), io=IO(out_verbs=("read",))
359
+ )
360
+ name = acol(
361
+ storage=S(type_=StrType, nullable=False),
362
+ io=IO(in_verbs=("create",), out_verbs=("read",)),
363
+ )
364
+
365
+ bind(Thing)
366
+ chains = build_phase_chains(Thing, "create")
367
+ assert "HANDLER" in chains
368
+ assert any(chains[ph] for ph in chains)
@@ -0,0 +1,181 @@
1
+ import pytest
2
+ import pytest_asyncio
3
+ from httpx import ASGITransport, AsyncClient
4
+ from sqlalchemy import select
5
+ from types import SimpleNamespace
6
+
7
+ from tigrbl import TigrblApp
8
+ from tigrbl.engine import resolver as _resolver
9
+ from tigrbl.engine.shortcuts import mem
10
+ from tigrbl.orm.tables import Base
11
+ from tigrbl.orm.mixins import GUIDPk
12
+ from tigrbl.specs import IO, S, acol
13
+ from tigrbl.types import App, String, UUID
14
+ from tigrbl.core import crud
15
+ from tigrbl.runtime.atoms.resolve import assemble
16
+
17
+
18
+ class Widget(Base, GUIDPk):
19
+ __tablename__ = "widgets"
20
+
21
+ name = acol(
22
+ storage=S(type_=String, nullable=False),
23
+ io=IO(in_verbs=("create",), out_verbs=("read",)),
24
+ )
25
+ secret = acol(
26
+ storage=S(type_=String, nullable=True),
27
+ io=IO(in_verbs=("create",), out_verbs=(), allow_out=False),
28
+ )
29
+ created_at = acol(
30
+ storage=S(type_=String, nullable=False),
31
+ io=IO(in_verbs=("create",), out_verbs=("read",)),
32
+ default_factory=lambda ctx: "now",
33
+ )
34
+
35
+
36
+ @pytest_asyncio.fixture
37
+ async def widget_setup():
38
+ app = App()
39
+ api = TigrblApp(engine=mem(async_=False))
40
+ api.include_model(Widget, prefix="/widget")
41
+ api.mount_jsonrpc(prefix="/rpc")
42
+ api.attach_diagnostics(prefix="/system")
43
+ api.initialize()
44
+ app.include_router(api.router)
45
+
46
+ prov = _resolver.resolve_provider()
47
+ SessionLocal = prov.session
48
+
49
+ transport = ASGITransport(app=app)
50
+ client = AsyncClient(transport=transport, base_url="http://test")
51
+ yield client, api, SessionLocal
52
+ await client.aclose()
53
+
54
+
55
+ @pytest.mark.i9n
56
+ @pytest.mark.asyncio
57
+ async def test_request_schema_reflects_io_spec(widget_setup):
58
+ _, api, _ = widget_setup
59
+ schema = api.schemas.Widget.create.in_.model_json_schema()
60
+ assert set(schema["properties"]) == {"name", "secret", "created_at"}
61
+
62
+
63
+ @pytest.mark.i9n
64
+ @pytest.mark.asyncio
65
+ async def test_response_schema_reflects_io_spec(widget_setup):
66
+ _, api, _ = widget_setup
67
+ schema = api.schemas.Widget.read.out.model_json_schema()
68
+ assert set(schema["properties"]) == {"id", "name", "created_at", "secret"}
69
+
70
+
71
+ @pytest.mark.i9n
72
+ @pytest.mark.asyncio
73
+ async def test_columns_store_io_spec(widget_setup):
74
+ _, _, _ = widget_setup
75
+ spec = Widget.__tigrbl_cols__["secret"].io
76
+ assert spec.allow_out is False
77
+
78
+
79
+ @pytest.mark.i9n
80
+ @pytest.mark.asyncio
81
+ async def test_default_factory_resolution(widget_setup):
82
+ _, _, _ = widget_setup
83
+ specs = Widget.__tigrbl_cols__
84
+ ctx = SimpleNamespace(
85
+ specs=specs, op="create", temp={"in_values": {}}, persist=True
86
+ )
87
+ assemble.run(None, ctx)
88
+ assert ctx.temp["assembled_values"]["created_at"] == "now"
89
+
90
+
91
+ @pytest.mark.i9n
92
+ @pytest.mark.asyncio
93
+ async def test_orm_model_carries_io_spec(widget_setup):
94
+ _, _, _ = widget_setup
95
+ assert "name" in Widget.__tigrbl_cols__
96
+
97
+
98
+ @pytest.mark.i9n
99
+ @pytest.mark.asyncio
100
+ async def test_openapi_reflects_io_spec(widget_setup):
101
+ client, _, _ = widget_setup
102
+ spec = (await client.get("/openapi.json")).json()
103
+ props = spec["components"]["schemas"]["WidgetReadResponse"]["properties"]
104
+ assert "secret" in props
105
+
106
+
107
+ @pytest.mark.i9n
108
+ @pytest.mark.asyncio
109
+ async def test_storage_persists_data(widget_setup):
110
+ client, _, SessionLocal = widget_setup
111
+ payload = {
112
+ "name": "hi",
113
+ "secret": "s",
114
+ "created_at": "now",
115
+ }
116
+ resp = await client.post("/widget/widget", json=payload)
117
+ wid = UUID(resp.json()["id"])
118
+ with SessionLocal() as session:
119
+ obj = session.execute(select(Widget).where(Widget.id == wid)).scalar_one()
120
+ assert obj.name == "hi"
121
+ assert obj.secret == "s"
122
+
123
+
124
+ @pytest.mark.i9n
125
+ @pytest.mark.asyncio
126
+ async def test_rest_calls_honor_io_spec(widget_setup):
127
+ client, _, _ = widget_setup
128
+ payload = {
129
+ "name": "hi",
130
+ "secret": "s",
131
+ "created_at": "now",
132
+ }
133
+ resp = await client.post("/widget/widget", json=payload)
134
+ wid = resp.json()["id"]
135
+ data = (await client.get(f"/widget/widget/{wid}")).json()
136
+ assert data["secret"] == "s"
137
+ assert data["name"] == "hi"
138
+
139
+
140
+ @pytest.mark.i9n
141
+ @pytest.mark.asyncio
142
+ async def test_rpc_methods_honor_io_spec(widget_setup):
143
+ client, _, _ = widget_setup
144
+ payload = {
145
+ "jsonrpc": "2.0",
146
+ "method": "Widget.create",
147
+ "params": {
148
+ "name": "rpc",
149
+ "secret": "x",
150
+ "created_at": "now",
151
+ },
152
+ "id": 1,
153
+ }
154
+ result = (await client.post("/rpc/", json=payload)).json()["result"]
155
+ assert result["secret"] == "x"
156
+
157
+
158
+ @pytest.mark.i9n
159
+ @pytest.mark.asyncio
160
+ async def test_core_crud_binding(widget_setup):
161
+ _, _, _ = widget_setup
162
+ assert Widget.hooks.create.HANDLER[0].__qualname__ == crud.create.__qualname__
163
+
164
+
165
+ @pytest.mark.i9n
166
+ @pytest.mark.asyncio
167
+ async def test_hookz_reports_operations(widget_setup):
168
+ client, _, _ = widget_setup
169
+ data = (await client.get("/system/hookz")).json()
170
+ assert "Widget" in data
171
+ assert "create" in data["Widget"]
172
+
173
+
174
+ @pytest.mark.i9n
175
+ @pytest.mark.asyncio
176
+ async def test_kernelz_lists_atoms_and_steps(widget_setup):
177
+ client, _, _ = widget_setup
178
+ data = (await client.get("/system/kernelz")).json()
179
+ steps = data["Widget"]["create"]
180
+ assert "HANDLER:hook:wire:tigrbl:core:crud:ops:create@HANDLER" in steps
181
+ assert any("hook:sys:txn:begin@START_TX" in s for s in steps)
@@ -0,0 +1,151 @@
1
+ from uuid import uuid4
2
+
3
+ import httpx
4
+ import pytest
5
+ import pytest_asyncio
6
+
7
+ from tigrbl import TigrblApp
8
+ from tigrbl.orm.mixins import (
9
+ GUIDPk,
10
+ Created,
11
+ LastUsed,
12
+ ValidityWindow,
13
+ KeyDigest,
14
+ tzutcnow,
15
+ tzutcnow_plus_day,
16
+ )
17
+ from tigrbl.orm.mixins.utils import CRUD_IO
18
+ from tigrbl.orm.tables._base import Base
19
+ from tigrbl.specs import F, S, acol
20
+ from tigrbl.types import App, Mapped, String
21
+ from sqlalchemy import inspect
22
+
23
+ from .uvicorn_utils import run_uvicorn_in_task, stop_uvicorn_server
24
+
25
+
26
+ class ApiKey(Base, GUIDPk, Created, LastUsed, ValidityWindow, KeyDigest):
27
+ __abstract__ = False
28
+ __tablename__ = "apikeys_uvicorn"
29
+ __resource__ = "apikey"
30
+
31
+ label: Mapped[str] = acol(
32
+ storage=S(String, nullable=False),
33
+ field=F(constraints={"max_length": 120}),
34
+ io=CRUD_IO,
35
+ )
36
+ service_id: Mapped[str] = acol(
37
+ storage=S(String, nullable=False),
38
+ field=F(constraints={"max_length": 120}),
39
+ io=CRUD_IO,
40
+ )
41
+
42
+
43
+ @pytest_asyncio.fixture()
44
+ async def running_app(sync_db_session):
45
+ engine, get_sync_db = sync_db_session
46
+
47
+ app = App()
48
+ api = TigrblApp(get_db=get_sync_db)
49
+ api.include_models([ApiKey])
50
+ await api.initialize()
51
+ app.include_router(api.router)
52
+
53
+ base_url, server, task = await run_uvicorn_in_task(app)
54
+ try:
55
+ yield (base_url, engine)
56
+ finally:
57
+ await stop_uvicorn_server(server, task)
58
+
59
+
60
+ def _payload() -> dict:
61
+ now = tzutcnow()
62
+ return {
63
+ "label": "test",
64
+ "service_id": str(uuid4()),
65
+ "valid_from": now.isoformat(),
66
+ "valid_to": tzutcnow_plus_day().isoformat(),
67
+ }
68
+
69
+
70
+ @pytest.mark.i9n
71
+ @pytest.mark.asyncio
72
+ async def test_create_apikey_success(running_app):
73
+ base_url, _ = running_app
74
+ async with httpx.AsyncClient() as client:
75
+ resp = await client.post(f"{base_url}/apikey", json=_payload())
76
+ assert resp.status_code == 201
77
+
78
+
79
+ @pytest.mark.i9n
80
+ @pytest.mark.asyncio
81
+ async def test_create_response_fields(running_app):
82
+ base_url, _ = running_app
83
+ async with httpx.AsyncClient() as client:
84
+ resp = await client.post(f"{base_url}/apikey", json=_payload())
85
+ body = resp.json()
86
+ expected = {
87
+ "api_key",
88
+ "label",
89
+ "service_id",
90
+ "valid_from",
91
+ "valid_to",
92
+ "digest",
93
+ "last_used_at",
94
+ "created_at",
95
+ "id",
96
+ }
97
+ assert set(body) == expected
98
+
99
+
100
+ @pytest.mark.i9n
101
+ @pytest.mark.asyncio
102
+ async def test_persisted_columns(running_app):
103
+ base_url, engine = running_app
104
+ async with httpx.AsyncClient() as client:
105
+ await client.post(f"{base_url}/apikey", json=_payload())
106
+ inspector = inspect(engine)
107
+ cols = {col["name"] for col in inspector.get_columns("apikeys_uvicorn")}
108
+ expected = {
109
+ "label",
110
+ "service_id",
111
+ "valid_from",
112
+ "valid_to",
113
+ "digest",
114
+ "last_used_at",
115
+ "created_at",
116
+ "id",
117
+ }
118
+ assert cols == expected
119
+
120
+
121
+ @pytest.mark.i9n
122
+ @pytest.mark.asyncio
123
+ async def test_read_excludes_api_key(running_app):
124
+ base_url, _ = running_app
125
+ async with httpx.AsyncClient() as client:
126
+ resp = await client.post(f"{base_url}/apikey", json=_payload())
127
+ created = resp.json()
128
+ fetched = await client.get(f"{base_url}/apikey/{created['id']}")
129
+ body = fetched.json()
130
+ assert "api_key" not in body
131
+ assert body["digest"] == created["digest"]
132
+
133
+
134
+ @pytest.mark.i9n
135
+ @pytest.mark.asyncio
136
+ async def test_rejects_digest_in_request(running_app):
137
+ base_url, _ = running_app
138
+ bad = _payload() | {"digest": "x"}
139
+ async with httpx.AsyncClient() as client:
140
+ resp = await client.post(f"{base_url}/apikey", json=bad)
141
+ assert resp.status_code == 422
142
+
143
+
144
+ @pytest.mark.i9n
145
+ @pytest.mark.asyncio
146
+ async def test_rejects_api_key_in_request(running_app):
147
+ base_url, _ = running_app
148
+ bad = _payload() | {"api_key": "raw"}
149
+ async with httpx.AsyncClient() as client:
150
+ resp = await client.post(f"{base_url}/apikey", json=bad)
151
+ assert resp.status_code == 422