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,20 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@pytest.mark.i9n
|
|
5
|
+
@pytest.mark.asyncio
|
|
6
|
+
async def test_list_filters_optional(api_client):
|
|
7
|
+
client, _, _ = api_client
|
|
8
|
+
|
|
9
|
+
spec = (await client.get("/openapi.json")).json()
|
|
10
|
+
params = spec["paths"]["/tenant"]["get"].get("parameters", [])
|
|
11
|
+
name_param = next(p for p in params if p["name"] == "name")
|
|
12
|
+
assert name_param["required"] is False
|
|
13
|
+
|
|
14
|
+
r = await client.get("/tenant")
|
|
15
|
+
assert r.status_code == 200
|
|
16
|
+
assert r.json() == []
|
|
17
|
+
|
|
18
|
+
r2 = await client.get("/tenant", params={"name": "foo"})
|
|
19
|
+
assert r2.status_code == 200
|
|
20
|
+
assert r2.json() == []
|
tests/i9n/test_mixins.py
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mixins Tests for Tigrbl v3
|
|
3
|
+
|
|
4
|
+
Tests all mixins and their expected behavior using individual DummyModel instances.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from tigrbl.types import String, uuid4
|
|
10
|
+
from tigrbl.column.shortcuts import acol, F, IO, S
|
|
11
|
+
|
|
12
|
+
from tigrbl import Base
|
|
13
|
+
from tigrbl.orm.mixins import (
|
|
14
|
+
ActiveToggle,
|
|
15
|
+
AsyncCapable,
|
|
16
|
+
Audited,
|
|
17
|
+
BulkCapable,
|
|
18
|
+
Created,
|
|
19
|
+
ExtRef,
|
|
20
|
+
GUIDPk,
|
|
21
|
+
LastUsed,
|
|
22
|
+
MetaJSON,
|
|
23
|
+
Monetary,
|
|
24
|
+
RelationEdge,
|
|
25
|
+
Replaceable,
|
|
26
|
+
Slugged,
|
|
27
|
+
SoftDelete,
|
|
28
|
+
StatusColumn,
|
|
29
|
+
Streamable,
|
|
30
|
+
Timestamped,
|
|
31
|
+
ValidityWindow,
|
|
32
|
+
Versioned,
|
|
33
|
+
tzutcnow,
|
|
34
|
+
tzutcnow_plus_day,
|
|
35
|
+
)
|
|
36
|
+
from tigrbl.schema import _build_schema
|
|
37
|
+
from tigrbl.engine import resolver as _resolver
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
NAME_FIELD = acol(
|
|
41
|
+
storage=S(type_=String, nullable=False),
|
|
42
|
+
field=F(py_type=str),
|
|
43
|
+
io=IO(
|
|
44
|
+
in_verbs=("create", "update", "replace"),
|
|
45
|
+
out_verbs=("read", "list"),
|
|
46
|
+
mutable_verbs=("create", "update", "replace"),
|
|
47
|
+
filter_ops=("eq",),
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DummyModelTimestamped(Base, GUIDPk, Timestamped):
|
|
53
|
+
"""Test model for Timestamped mixin."""
|
|
54
|
+
|
|
55
|
+
__tablename__ = "dummy_timestamped"
|
|
56
|
+
name = NAME_FIELD
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class DummyModelCreated(Base, GUIDPk, Created):
|
|
60
|
+
"""Test model for Created mixin."""
|
|
61
|
+
|
|
62
|
+
__tablename__ = "dummy_created"
|
|
63
|
+
name = NAME_FIELD
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DummyModelLastUsed(Base, GUIDPk, LastUsed):
|
|
67
|
+
"""Test model for LastUsed mixin."""
|
|
68
|
+
|
|
69
|
+
__tablename__ = "dummy_last_used"
|
|
70
|
+
name = NAME_FIELD
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class DummyModelActiveToggle(Base, GUIDPk, ActiveToggle):
|
|
74
|
+
"""Test model for ActiveToggle mixin."""
|
|
75
|
+
|
|
76
|
+
__tablename__ = "dummy_active_toggle"
|
|
77
|
+
name = NAME_FIELD
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class DummyModelSoftDelete(Base, GUIDPk, SoftDelete):
|
|
81
|
+
"""Test model for SoftDelete mixin."""
|
|
82
|
+
|
|
83
|
+
__tablename__ = "dummy_soft_delete"
|
|
84
|
+
name = NAME_FIELD
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class DummyModelVersioned(Base, GUIDPk, Versioned):
|
|
88
|
+
"""Test model for Versioned mixin."""
|
|
89
|
+
|
|
90
|
+
__tablename__ = "dummy_versioned"
|
|
91
|
+
name = NAME_FIELD
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class DummyModelBulkCapable(Base, GUIDPk, BulkCapable):
|
|
95
|
+
"""Test model for BulkCapable mixin."""
|
|
96
|
+
|
|
97
|
+
__tablename__ = "dummy_bulk_capable"
|
|
98
|
+
name = NAME_FIELD
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class DummyModelReplaceable(Base, GUIDPk, Replaceable):
|
|
102
|
+
"""Test model for Replaceable mixin."""
|
|
103
|
+
|
|
104
|
+
__tablename__ = "dummy_replaceable"
|
|
105
|
+
name = NAME_FIELD
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class DummyModelAsyncCapable(Base, GUIDPk, AsyncCapable):
|
|
109
|
+
"""Test model for AsyncCapable mixin."""
|
|
110
|
+
|
|
111
|
+
__tablename__ = "dummy_async_capable"
|
|
112
|
+
name = NAME_FIELD
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class DummyModelSlugged(Base, GUIDPk, Slugged):
|
|
116
|
+
"""Test model for Slugged mixin."""
|
|
117
|
+
|
|
118
|
+
__tablename__ = "dummy_slugged"
|
|
119
|
+
name = NAME_FIELD
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class DummyModelStatusColumn(Base, GUIDPk, StatusColumn):
|
|
123
|
+
"""Test model for StatusColumn."""
|
|
124
|
+
|
|
125
|
+
__tablename__ = "dummy_status_column"
|
|
126
|
+
name = NAME_FIELD
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class DummyModelValidityWindow(Base, GUIDPk, ValidityWindow):
|
|
130
|
+
"""Test model for ValidityWindow mixin."""
|
|
131
|
+
|
|
132
|
+
__tablename__ = "dummy_validity_window"
|
|
133
|
+
name = NAME_FIELD
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class DummyModelMonetary(Base, GUIDPk, Monetary):
|
|
137
|
+
"""Test model for Monetary mixin."""
|
|
138
|
+
|
|
139
|
+
__tablename__ = "dummy_monetary"
|
|
140
|
+
name = NAME_FIELD
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class DummyModelExtRef(Base, GUIDPk, ExtRef):
|
|
144
|
+
"""Test model for ExtRef mixin."""
|
|
145
|
+
|
|
146
|
+
__tablename__ = "dummy_ext_ref"
|
|
147
|
+
name = NAME_FIELD
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class DummyModelMetaJSON(Base, GUIDPk, MetaJSON):
|
|
151
|
+
"""Test model for MetaJSON mixin."""
|
|
152
|
+
|
|
153
|
+
__tablename__ = "dummy_meta_json"
|
|
154
|
+
name = NAME_FIELD
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@pytest.mark.i9n
|
|
158
|
+
@pytest.mark.asyncio
|
|
159
|
+
async def test_timestamped_mixin(create_test_api):
|
|
160
|
+
"""Test that Timestamped mixin adds created_at and updated_at fields."""
|
|
161
|
+
create_test_api(DummyModelTimestamped)
|
|
162
|
+
|
|
163
|
+
# Get schemas
|
|
164
|
+
create_schema = _build_schema(DummyModelTimestamped, verb="create")
|
|
165
|
+
read_schema = _build_schema(DummyModelTimestamped, verb="read")
|
|
166
|
+
update_schema = _build_schema(DummyModelTimestamped, verb="update")
|
|
167
|
+
|
|
168
|
+
# created_at and updated_at are read-only and appear only in read schema
|
|
169
|
+
assert "created_at" in read_schema.model_fields
|
|
170
|
+
assert "updated_at" in read_schema.model_fields
|
|
171
|
+
assert "created_at" not in create_schema.model_fields
|
|
172
|
+
assert "updated_at" not in create_schema.model_fields
|
|
173
|
+
assert "created_at" not in update_schema.model_fields
|
|
174
|
+
assert "updated_at" not in update_schema.model_fields
|
|
175
|
+
|
|
176
|
+
# name should be in all schemas
|
|
177
|
+
assert "name" in create_schema.model_fields
|
|
178
|
+
assert "name" in read_schema.model_fields
|
|
179
|
+
assert "name" in update_schema.model_fields
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@pytest.mark.i9n
|
|
183
|
+
@pytest.mark.asyncio
|
|
184
|
+
async def test_created_mixin(create_test_api):
|
|
185
|
+
"""Test that Created mixin adds created_at field."""
|
|
186
|
+
create_test_api(DummyModelCreated)
|
|
187
|
+
|
|
188
|
+
# Get schemas
|
|
189
|
+
create_schema = _build_schema(DummyModelCreated, verb="create")
|
|
190
|
+
read_schema = _build_schema(DummyModelCreated, verb="read")
|
|
191
|
+
|
|
192
|
+
# created_at is read-only and only present in read schema
|
|
193
|
+
assert "created_at" in read_schema.model_fields
|
|
194
|
+
assert "created_at" not in create_schema.model_fields
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@pytest.mark.i9n
|
|
198
|
+
@pytest.mark.asyncio
|
|
199
|
+
async def test_last_used_mixin(create_test_api):
|
|
200
|
+
"""Test that LastUsed mixin adds last_used_at field and touch method."""
|
|
201
|
+
create_test_api(DummyModelLastUsed)
|
|
202
|
+
|
|
203
|
+
# Get schemas
|
|
204
|
+
read_schema = _build_schema(DummyModelLastUsed, verb="read")
|
|
205
|
+
|
|
206
|
+
# last_used_at should be in read schema
|
|
207
|
+
assert "last_used_at" in read_schema.model_fields
|
|
208
|
+
|
|
209
|
+
# Verify the model has touch method
|
|
210
|
+
assert hasattr(DummyModelLastUsed, "touch")
|
|
211
|
+
|
|
212
|
+
# Test touch method functionality
|
|
213
|
+
instance = DummyModelLastUsed(name="test")
|
|
214
|
+
assert instance.last_used_at is None
|
|
215
|
+
|
|
216
|
+
instance.touch()
|
|
217
|
+
assert instance.last_used_at is not None
|
|
218
|
+
assert isinstance(instance.last_used_at, datetime)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@pytest.mark.i9n
|
|
222
|
+
@pytest.mark.asyncio
|
|
223
|
+
async def test_active_toggle_mixin(create_test_api):
|
|
224
|
+
"""Test that ActiveToggle mixin adds is_active field."""
|
|
225
|
+
create_test_api(DummyModelActiveToggle)
|
|
226
|
+
|
|
227
|
+
# Get schemas
|
|
228
|
+
create_schema = _build_schema(DummyModelActiveToggle, verb="create")
|
|
229
|
+
read_schema = _build_schema(DummyModelActiveToggle, verb="read")
|
|
230
|
+
|
|
231
|
+
# is_active should be in schemas
|
|
232
|
+
assert "is_active" in create_schema.model_fields
|
|
233
|
+
assert "is_active" in read_schema.model_fields
|
|
234
|
+
|
|
235
|
+
# is_active field should be boolean type (default may be None)
|
|
236
|
+
is_active_field = create_schema.model_fields["is_active"]
|
|
237
|
+
assert is_active_field.annotation is bool
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@pytest.mark.i9n
|
|
241
|
+
@pytest.mark.asyncio
|
|
242
|
+
async def test_soft_delete_mixin(create_test_api):
|
|
243
|
+
"""Test that SoftDelete mixin adds deleted_at field."""
|
|
244
|
+
create_test_api(DummyModelSoftDelete)
|
|
245
|
+
|
|
246
|
+
# Get schemas
|
|
247
|
+
read_schema = _build_schema(DummyModelSoftDelete, verb="read")
|
|
248
|
+
|
|
249
|
+
# deleted_at should be in read schema
|
|
250
|
+
assert "deleted_at" in read_schema.model_fields
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@pytest.mark.i9n
|
|
254
|
+
@pytest.mark.asyncio
|
|
255
|
+
async def test_versioned_mixin(create_test_api):
|
|
256
|
+
"""Test that Versioned mixin adds revision and prev_id fields."""
|
|
257
|
+
create_test_api(DummyModelVersioned)
|
|
258
|
+
|
|
259
|
+
# Get schemas
|
|
260
|
+
create_schema = _build_schema(DummyModelVersioned, verb="create")
|
|
261
|
+
read_schema = _build_schema(DummyModelVersioned, verb="read")
|
|
262
|
+
|
|
263
|
+
# revision and prev_id should be in schemas
|
|
264
|
+
assert "revision" in read_schema.model_fields
|
|
265
|
+
assert "prev_id" in read_schema.model_fields
|
|
266
|
+
|
|
267
|
+
# revision should have default value of 1
|
|
268
|
+
revision_field = create_schema.model_fields["revision"]
|
|
269
|
+
assert revision_field.annotation is int
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@pytest.mark.i9n
|
|
273
|
+
@pytest.mark.asyncio
|
|
274
|
+
async def test_bulk_capable_mixin(create_test_api):
|
|
275
|
+
"""Test that BulkCapable mixin enables bulk operations."""
|
|
276
|
+
api = create_test_api(DummyModelBulkCapable)
|
|
277
|
+
|
|
278
|
+
# Check that bulk routes are available
|
|
279
|
+
routes = [route.path for route in api.router.routes]
|
|
280
|
+
|
|
281
|
+
# Bulk operations now share the base collection path
|
|
282
|
+
expected_path = f"/{DummyModelBulkCapable.__name__.lower()}"
|
|
283
|
+
assert expected_path in routes
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@pytest.mark.i9n
|
|
287
|
+
@pytest.mark.asyncio
|
|
288
|
+
async def test_replaceable_mixin(create_test_api):
|
|
289
|
+
"""Test that Replaceable mixin enables replacement operations."""
|
|
290
|
+
create_test_api(DummyModelReplaceable)
|
|
291
|
+
|
|
292
|
+
# Get schemas
|
|
293
|
+
create_schema = _build_schema(DummyModelReplaceable, verb="create")
|
|
294
|
+
read_schema = _build_schema(DummyModelReplaceable, verb="read")
|
|
295
|
+
|
|
296
|
+
# Should have basic fields
|
|
297
|
+
assert "name" in create_schema.model_fields
|
|
298
|
+
assert "name" in read_schema.model_fields
|
|
299
|
+
|
|
300
|
+
# Replaceable mixin is a marker mixin - doesn't add fields
|
|
301
|
+
# but enables replacement functionality
|
|
302
|
+
expected_fields = {"id", "name"}
|
|
303
|
+
actual_fields = set(read_schema.model_fields.keys())
|
|
304
|
+
assert expected_fields.issubset(actual_fields)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@pytest.mark.i9n
|
|
308
|
+
@pytest.mark.asyncio
|
|
309
|
+
async def test_async_capable_mixin(create_test_api):
|
|
310
|
+
"""Test that AsyncCapable mixin is a marker mixin."""
|
|
311
|
+
create_test_api(DummyModelAsyncCapable)
|
|
312
|
+
|
|
313
|
+
# Get schemas
|
|
314
|
+
read_schema = _build_schema(DummyModelAsyncCapable, verb="read")
|
|
315
|
+
|
|
316
|
+
# AsyncCapable is a marker mixin - doesn't add fields
|
|
317
|
+
expected_fields = {"id", "name"}
|
|
318
|
+
actual_fields = set(read_schema.model_fields.keys())
|
|
319
|
+
assert actual_fields == expected_fields
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@pytest.mark.i9n
|
|
323
|
+
@pytest.mark.asyncio
|
|
324
|
+
async def test_slugged_mixin(create_test_api):
|
|
325
|
+
"""Test that Slugged mixin adds slug field."""
|
|
326
|
+
create_test_api(DummyModelSlugged)
|
|
327
|
+
|
|
328
|
+
# Get schemas
|
|
329
|
+
create_schema = _build_schema(DummyModelSlugged, verb="create")
|
|
330
|
+
read_schema = _build_schema(DummyModelSlugged, verb="read")
|
|
331
|
+
|
|
332
|
+
# slug should be in schemas
|
|
333
|
+
assert "slug" in create_schema.model_fields
|
|
334
|
+
assert "slug" in read_schema.model_fields
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@pytest.mark.i9n
|
|
338
|
+
@pytest.mark.asyncio
|
|
339
|
+
async def test_status_column(create_test_api):
|
|
340
|
+
"""Test that StatusColumn adds status field."""
|
|
341
|
+
create_test_api(DummyModelStatusColumn)
|
|
342
|
+
|
|
343
|
+
# Get schemas
|
|
344
|
+
create_schema = _build_schema(DummyModelStatusColumn, verb="create")
|
|
345
|
+
read_schema = _build_schema(DummyModelStatusColumn, verb="read")
|
|
346
|
+
|
|
347
|
+
# status should be in schemas
|
|
348
|
+
assert "status" in create_schema.model_fields
|
|
349
|
+
assert "status" in read_schema.model_fields
|
|
350
|
+
|
|
351
|
+
# status field should be string type
|
|
352
|
+
status_field = create_schema.model_fields["status"]
|
|
353
|
+
assert status_field.annotation is str
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@pytest.mark.i9n
|
|
357
|
+
@pytest.mark.asyncio
|
|
358
|
+
async def test_validity_window_mixin(create_test_api):
|
|
359
|
+
"""Test that ValidityWindow mixin adds valid_from and valid_until fields."""
|
|
360
|
+
create_test_api(DummyModelValidityWindow)
|
|
361
|
+
|
|
362
|
+
# Get schemas
|
|
363
|
+
create_schema = _build_schema(DummyModelValidityWindow, verb="create")
|
|
364
|
+
read_schema = _build_schema(DummyModelValidityWindow, verb="read")
|
|
365
|
+
|
|
366
|
+
# validity fields should be in schemas
|
|
367
|
+
assert "valid_from" in create_schema.model_fields
|
|
368
|
+
assert "valid_to" in create_schema.model_fields
|
|
369
|
+
assert "valid_from" in read_schema.model_fields
|
|
370
|
+
assert "valid_to" in read_schema.model_fields
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@pytest.mark.i9n
|
|
374
|
+
@pytest.mark.asyncio
|
|
375
|
+
async def test_validity_window_default(create_test_api):
|
|
376
|
+
api = create_test_api(DummyModelValidityWindow)
|
|
377
|
+
session, release = _resolver.acquire(api=api)
|
|
378
|
+
try:
|
|
379
|
+
vf_default = tzutcnow()
|
|
380
|
+
vt_default = tzutcnow_plus_day()
|
|
381
|
+
instance = DummyModelValidityWindow(
|
|
382
|
+
id=uuid4(), name="x", valid_from=vf_default, valid_to=vt_default
|
|
383
|
+
)
|
|
384
|
+
session.add(instance)
|
|
385
|
+
session.flush()
|
|
386
|
+
finally:
|
|
387
|
+
release()
|
|
388
|
+
assert vf_default is not None
|
|
389
|
+
assert vt_default is not None
|
|
390
|
+
assert abs((vt_default - vf_default) - timedelta(days=1)) < timedelta(seconds=1)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@pytest.mark.i9n
|
|
394
|
+
@pytest.mark.asyncio
|
|
395
|
+
async def test_tzutcnow():
|
|
396
|
+
"""tzutcnow returns an aware UTC datetime close to current time."""
|
|
397
|
+
now = tzutcnow()
|
|
398
|
+
assert now.tzinfo == timezone.utc
|
|
399
|
+
assert abs(now - datetime.now(timezone.utc)) < timedelta(seconds=1)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@pytest.mark.i9n
|
|
403
|
+
@pytest.mark.asyncio
|
|
404
|
+
async def test_tzutcnow_plus_day():
|
|
405
|
+
"""tzutcnow_plus_day returns an aware UTC datetime one day ahead."""
|
|
406
|
+
now = tzutcnow()
|
|
407
|
+
future = tzutcnow_plus_day()
|
|
408
|
+
assert future.tzinfo == timezone.utc
|
|
409
|
+
assert future > now
|
|
410
|
+
assert abs((future - now) - timedelta(days=1)) < timedelta(seconds=1)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@pytest.mark.i9n
|
|
414
|
+
@pytest.mark.asyncio
|
|
415
|
+
async def test_monetary_mixin(create_test_api):
|
|
416
|
+
"""Test that Monetary mixin adds currency and amount fields."""
|
|
417
|
+
create_test_api(DummyModelMonetary)
|
|
418
|
+
|
|
419
|
+
# Get schemas
|
|
420
|
+
create_schema = _build_schema(DummyModelMonetary, verb="create")
|
|
421
|
+
read_schema = _build_schema(DummyModelMonetary, verb="read")
|
|
422
|
+
|
|
423
|
+
# monetary fields should be in schemas
|
|
424
|
+
assert "currency" in create_schema.model_fields
|
|
425
|
+
assert "amount" in create_schema.model_fields
|
|
426
|
+
assert "currency" in read_schema.model_fields
|
|
427
|
+
assert "amount" in read_schema.model_fields
|
|
428
|
+
|
|
429
|
+
# currency field should be string type
|
|
430
|
+
currency_field = create_schema.model_fields["currency"]
|
|
431
|
+
assert currency_field.annotation is str
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@pytest.mark.i9n
|
|
435
|
+
@pytest.mark.asyncio
|
|
436
|
+
async def test_ext_ref_mixin(create_test_api):
|
|
437
|
+
"""Test that ExtRef mixin adds external_id field."""
|
|
438
|
+
create_test_api(DummyModelExtRef)
|
|
439
|
+
|
|
440
|
+
# Get schemas
|
|
441
|
+
create_schema = _build_schema(DummyModelExtRef, verb="create")
|
|
442
|
+
read_schema = _build_schema(DummyModelExtRef, verb="read")
|
|
443
|
+
|
|
444
|
+
# external_id should be in schemas
|
|
445
|
+
assert "external_id" in create_schema.model_fields
|
|
446
|
+
assert "external_id" in read_schema.model_fields
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@pytest.mark.i9n
|
|
450
|
+
@pytest.mark.asyncio
|
|
451
|
+
@pytest.mark.skip(reason="JSONB type not supported in SQLite test environment")
|
|
452
|
+
async def test_meta_json_mixin(create_test_api):
|
|
453
|
+
"""Test that MetaJSON mixin adds meta field."""
|
|
454
|
+
create_test_api(DummyModelMetaJSON)
|
|
455
|
+
|
|
456
|
+
# Get schemas
|
|
457
|
+
create_schema = _build_schema(DummyModelMetaJSON, verb="create")
|
|
458
|
+
read_schema = _build_schema(DummyModelMetaJSON, verb="read")
|
|
459
|
+
|
|
460
|
+
# meta should be in schemas
|
|
461
|
+
assert "meta" in create_schema.model_fields
|
|
462
|
+
assert "meta" in read_schema.model_fields
|
|
463
|
+
|
|
464
|
+
# meta should default to empty dict
|
|
465
|
+
meta_field = create_schema.model_fields["meta"]
|
|
466
|
+
assert meta_field.default == {}
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@pytest.mark.i9n
|
|
470
|
+
@pytest.mark.asyncio
|
|
471
|
+
async def test_marker_mixins(create_test_api):
|
|
472
|
+
"""Test that marker mixins (Audited, Streamable, etc.) don't add fields."""
|
|
473
|
+
|
|
474
|
+
# Create dummy models for other marker mixins
|
|
475
|
+
class DummyAudited(Base, GUIDPk, Audited):
|
|
476
|
+
__tablename__ = "dummy_audited"
|
|
477
|
+
name = NAME_FIELD
|
|
478
|
+
|
|
479
|
+
class DummyStreamable(Base, GUIDPk, Streamable):
|
|
480
|
+
__tablename__ = "dummy_streamable"
|
|
481
|
+
name = NAME_FIELD
|
|
482
|
+
|
|
483
|
+
class DummyRelationEdge(Base, GUIDPk, RelationEdge):
|
|
484
|
+
__tablename__ = "dummy_relation_edge"
|
|
485
|
+
name = NAME_FIELD
|
|
486
|
+
|
|
487
|
+
marker_models = [DummyAudited, DummyStreamable, DummyRelationEdge]
|
|
488
|
+
|
|
489
|
+
for model in marker_models:
|
|
490
|
+
create_test_api(model)
|
|
491
|
+
|
|
492
|
+
read_schema = _build_schema(model, verb="read")
|
|
493
|
+
|
|
494
|
+
# Should only have id and name fields (no extra fields from marker mixins)
|
|
495
|
+
expected_fields = {"id", "name"}
|
|
496
|
+
actual_fields = set(read_schema.model_fields.keys())
|
|
497
|
+
assert actual_fields == expected_fields
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
@pytest.mark.i9n
|
|
501
|
+
@pytest.mark.asyncio
|
|
502
|
+
async def test_multiple_mixins_combination(create_test_api):
|
|
503
|
+
"""Test that multiple mixins can be combined correctly."""
|
|
504
|
+
|
|
505
|
+
class DummyMultipleMixins(
|
|
506
|
+
Base, GUIDPk, Timestamped, ActiveToggle, Slugged, StatusColumn
|
|
507
|
+
):
|
|
508
|
+
__tablename__ = "dummy_multiple_mixins"
|
|
509
|
+
name = NAME_FIELD
|
|
510
|
+
|
|
511
|
+
create_test_api(DummyMultipleMixins)
|
|
512
|
+
|
|
513
|
+
# Get schemas
|
|
514
|
+
create_schema = _build_schema(DummyMultipleMixins, verb="create")
|
|
515
|
+
read_schema = _build_schema(DummyMultipleMixins, verb="read")
|
|
516
|
+
|
|
517
|
+
# Should have fields from all mixins
|
|
518
|
+
# From ActiveToggle
|
|
519
|
+
assert "is_active" in create_schema.model_fields
|
|
520
|
+
assert "is_active" in read_schema.model_fields
|
|
521
|
+
|
|
522
|
+
# From Slugged
|
|
523
|
+
assert "slug" in create_schema.model_fields
|
|
524
|
+
assert "slug" in read_schema.model_fields
|
|
525
|
+
|
|
526
|
+
# From StatusColumn
|
|
527
|
+
assert "status" in create_schema.model_fields
|
|
528
|
+
assert "status" in read_schema.model_fields
|
|
529
|
+
|
|
530
|
+
# From Timestamped - read-only fields should only appear in read schema
|
|
531
|
+
assert "created_at" not in create_schema.model_fields
|
|
532
|
+
assert "updated_at" not in create_schema.model_fields
|
|
533
|
+
assert "created_at" in read_schema.model_fields
|
|
534
|
+
assert "updated_at" in read_schema.model_fields
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@pytest.mark.i9n
|
|
5
|
+
@pytest.mark.asyncio
|
|
6
|
+
async def test_nested_path_schema_and_rpc(api_client):
|
|
7
|
+
client, _, Item = api_client
|
|
8
|
+
|
|
9
|
+
# Create a tenant
|
|
10
|
+
tenant_res = await client.post("/tenant", json={"name": "Acme"})
|
|
11
|
+
tenant_res.raise_for_status()
|
|
12
|
+
tenant_id = tenant_res.json()["id"]
|
|
13
|
+
|
|
14
|
+
# Schema should mark parent identifiers optional
|
|
15
|
+
create_model = Item.schemas.create.in_
|
|
16
|
+
fields = getattr(create_model, "model_fields", None)
|
|
17
|
+
if fields is None:
|
|
18
|
+
fields = getattr(create_model, "__fields__", {})
|
|
19
|
+
assert "tenant_id" not in fields
|
|
20
|
+
|
|
21
|
+
# REST call should inject path params
|
|
22
|
+
rest_payload = [create_model(name="rest-item").model_dump(exclude_none=True)]
|
|
23
|
+
rest_res = await client.post(f"/tenant/{tenant_id}/item", json=rest_payload)
|
|
24
|
+
rest_res.raise_for_status()
|
|
25
|
+
rest_item = rest_res.json()[0]
|
|
26
|
+
assert rest_item["tenant_id"] == tenant_id
|
|
27
|
+
|
|
28
|
+
# RPC call should succeed when tenant_id is provided explicitly
|
|
29
|
+
rpc_payload = {
|
|
30
|
+
"method": "Item.create",
|
|
31
|
+
"params": {"tenant_id": tenant_id, "name": "rpc-item"},
|
|
32
|
+
}
|
|
33
|
+
rpc_res = await client.post("/rpc", json=rpc_payload)
|
|
34
|
+
rpc_res.raise_for_status()
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import pytest_asyncio
|
|
3
|
+
from tigrbl import TigrblApp, Base
|
|
4
|
+
from tigrbl.engine.shortcuts import mem
|
|
5
|
+
from tigrbl.orm.mixins import GUIDPk
|
|
6
|
+
from tigrbl.types import App
|
|
7
|
+
from httpx import ASGITransport, AsyncClient
|
|
8
|
+
from sqlalchemy import Column, ForeignKey, String
|
|
9
|
+
from tigrbl.types import PgUUID
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest_asyncio.fixture
|
|
13
|
+
async def three_level_api_client(db_mode):
|
|
14
|
+
Base.metadata.clear()
|
|
15
|
+
Base.registry.dispose()
|
|
16
|
+
|
|
17
|
+
class Company(Base, GUIDPk):
|
|
18
|
+
__tablename__ = "companies"
|
|
19
|
+
name = Column(String, nullable=False)
|
|
20
|
+
|
|
21
|
+
class Department(Base, GUIDPk):
|
|
22
|
+
__tablename__ = "departments"
|
|
23
|
+
company_id = Column(
|
|
24
|
+
PgUUID(as_uuid=True), ForeignKey("companies.id"), nullable=False
|
|
25
|
+
)
|
|
26
|
+
name = Column(String, nullable=False)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def __tigrbl_nested_paths__(cls):
|
|
30
|
+
return "/company/{company_id}/department"
|
|
31
|
+
|
|
32
|
+
class Employee(Base, GUIDPk):
|
|
33
|
+
__tablename__ = "employees"
|
|
34
|
+
company_id = Column(
|
|
35
|
+
PgUUID(as_uuid=True), ForeignKey("companies.id"), nullable=False
|
|
36
|
+
)
|
|
37
|
+
department_id = Column(
|
|
38
|
+
PgUUID(as_uuid=True), ForeignKey("departments.id"), nullable=False
|
|
39
|
+
)
|
|
40
|
+
name = Column(String, nullable=False)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def __tigrbl_nested_paths__(cls):
|
|
44
|
+
return "/company/{company_id}/department/{department_id}/employee"
|
|
45
|
+
|
|
46
|
+
if db_mode == "async":
|
|
47
|
+
pytest.skip("async database mode is currently unsupported")
|
|
48
|
+
else:
|
|
49
|
+
api = TigrblApp(engine=mem(async_=False))
|
|
50
|
+
api.include_models([Company, Department, Employee])
|
|
51
|
+
api.initialize()
|
|
52
|
+
|
|
53
|
+
app = App()
|
|
54
|
+
app.include_router(api.router)
|
|
55
|
+
transport = ASGITransport(app=app)
|
|
56
|
+
client = AsyncClient(transport=transport, base_url="http://test")
|
|
57
|
+
return client
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.i9n
|
|
61
|
+
@pytest.mark.asyncio
|
|
62
|
+
async def test_nested_routing_depth(three_level_api_client):
|
|
63
|
+
client = three_level_api_client
|
|
64
|
+
|
|
65
|
+
# Create company
|
|
66
|
+
res = await client.post("/company", json={"name": "Acme"})
|
|
67
|
+
assert res.status_code == 201
|
|
68
|
+
company_id = res.json()["id"]
|
|
69
|
+
|
|
70
|
+
# Create department
|
|
71
|
+
res = await client.post(
|
|
72
|
+
f"/company/{company_id}/department",
|
|
73
|
+
json={"name": "Engineering"},
|
|
74
|
+
)
|
|
75
|
+
assert res.status_code == 201
|
|
76
|
+
department_id = res.json()["id"]
|
|
77
|
+
|
|
78
|
+
# Create employee
|
|
79
|
+
res = await client.post(
|
|
80
|
+
f"/company/{company_id}/department/{department_id}/employee",
|
|
81
|
+
json={"name": "Alice"},
|
|
82
|
+
)
|
|
83
|
+
assert res.status_code == 201
|
|
84
|
+
employee_id = res.json()["id"]
|
|
85
|
+
|
|
86
|
+
# Verify generated REST paths and HTTP methods
|
|
87
|
+
paths = (await client.get("/openapi.json")).json()["paths"]
|
|
88
|
+
expected = {
|
|
89
|
+
"/company": {"post", "get", "delete"},
|
|
90
|
+
"/company/{item_id}": {"get", "patch", "delete"},
|
|
91
|
+
"/company/{company_id}/department": {"post", "get", "delete"},
|
|
92
|
+
"/company/{company_id}/department/{item_id}": {"get", "patch", "delete"},
|
|
93
|
+
"/company/{company_id}/department/{department_id}/employee": {
|
|
94
|
+
"post",
|
|
95
|
+
"get",
|
|
96
|
+
"delete",
|
|
97
|
+
},
|
|
98
|
+
"/company/{company_id}/department/{department_id}/employee/{item_id}": {
|
|
99
|
+
"get",
|
|
100
|
+
"patch",
|
|
101
|
+
"delete",
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
for path, verbs in expected.items():
|
|
105
|
+
assert path in paths
|
|
106
|
+
for verb in verbs:
|
|
107
|
+
assert verb in paths[path]
|
|
108
|
+
|
|
109
|
+
# Confirm nested routes resolve to correct handlers
|
|
110
|
+
res = await client.get(f"/company/{company_id}/department/{department_id}")
|
|
111
|
+
assert res.status_code == 200
|
|
112
|
+
assert res.json()["id"] == department_id
|
|
113
|
+
|
|
114
|
+
res = await client.get(
|
|
115
|
+
f"/company/{company_id}/department/{department_id}/employee/{employee_id}"
|
|
116
|
+
)
|
|
117
|
+
assert res.status_code == 200
|
|
118
|
+
assert res.json()["id"] == employee_id
|