mockstack 0.0.3__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.
- mockstack/__init__.py +0 -0
- mockstack/config.py +91 -0
- mockstack/display.py +35 -0
- mockstack/identifiers.py +84 -0
- mockstack/lifespan.py +29 -0
- mockstack/main.py +31 -0
- mockstack/middleware.py +19 -0
- mockstack/opentelemetry.py +45 -0
- mockstack/routers/__init__.py +0 -0
- mockstack/routers/catchall.py +33 -0
- mockstack/routers/homepage.py +18 -0
- mockstack/strategies/__init__.py +0 -0
- mockstack/strategies/base.py +14 -0
- mockstack/strategies/factory.py +21 -0
- mockstack/strategies/filefixtures.py +230 -0
- mockstack/templating.py +114 -0
- mockstack/tests/__init__.py +0 -0
- mockstack/tests/conftest.py +29 -0
- mockstack/tests/fixtures/templates/__init__.py +0 -0
- mockstack/tests/routers/__init__.py +0 -0
- mockstack/tests/routers/test_catchall.py +46 -0
- mockstack/tests/routers/test_homepage.py +27 -0
- mockstack/tests/strategies/test_filefixtures.py +312 -0
- mockstack/tests/test_display.py +33 -0
- mockstack/tests/test_identifiers.py +94 -0
- mockstack/tests/test_middleware.py +37 -0
- mockstack/tests/test_templating.py +220 -0
- mockstack-0.0.3.dist-info/METADATA +13 -0
- mockstack-0.0.3.dist-info/RECORD +32 -0
- mockstack-0.0.3.dist-info/WHEEL +5 -0
- mockstack-0.0.3.dist-info/licenses/LICENSE +21 -0
- mockstack-0.0.3.dist-info/top_level.txt +1 -0
mockstack/templating.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Templates related functionality."""
|
|
2
|
+
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Generator
|
|
6
|
+
|
|
7
|
+
from fastapi import Request
|
|
8
|
+
|
|
9
|
+
from mockstack.identifiers import looks_like_id, prefixes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def missing_template_detail(request: Request, *, templates_dir: Path) -> str:
|
|
13
|
+
"""Return a detailed message for a missing template."""
|
|
14
|
+
return (
|
|
15
|
+
"Template not found for given request. "
|
|
16
|
+
f"path: {request.url.path}, "
|
|
17
|
+
f"query: {request.query_params}, "
|
|
18
|
+
f"templates_dir: {templates_dir}, "
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def iter_possible_template_arguments(
|
|
23
|
+
request: Request,
|
|
24
|
+
default_identifier_key: str = "id",
|
|
25
|
+
default_media_type: str = "application/json",
|
|
26
|
+
default_template_name: str = "index.j2",
|
|
27
|
+
template_file_separator: str = "-",
|
|
28
|
+
template_file_extension: str = ".j2",
|
|
29
|
+
) -> Generator[dict, None, None]:
|
|
30
|
+
"""Infer the template arguments for a given request.
|
|
31
|
+
|
|
32
|
+
This includes:
|
|
33
|
+
|
|
34
|
+
- Inferring the name for the template file from the URL
|
|
35
|
+
- Inferring the context variables available for the template from the URL and request body.
|
|
36
|
+
- Inferring the response (media) type for the template from the URL and request body.
|
|
37
|
+
|
|
38
|
+
There is a fair amount of extrapolation happening here. The philosophy is to provide
|
|
39
|
+
a behavior that "just works" for the majority of the cases encountered in practice.
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
path = request.url.path
|
|
43
|
+
|
|
44
|
+
name_segments, context = parse_template_name_segments_and_context(
|
|
45
|
+
path,
|
|
46
|
+
default_identifier_key=default_identifier_key,
|
|
47
|
+
)
|
|
48
|
+
media_type = request.headers.get("Content-Type", default_media_type)
|
|
49
|
+
|
|
50
|
+
template_name_kwargs = dict(
|
|
51
|
+
template_file_separator=template_file_separator,
|
|
52
|
+
template_file_extension=template_file_extension,
|
|
53
|
+
default_template_name=default_template_name,
|
|
54
|
+
)
|
|
55
|
+
for name in iter_possible_template_filenames(
|
|
56
|
+
name_segments, context, **template_name_kwargs
|
|
57
|
+
):
|
|
58
|
+
yield dict(
|
|
59
|
+
name=name,
|
|
60
|
+
context=context,
|
|
61
|
+
media_type=media_type,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse_template_name_segments_and_context(
|
|
66
|
+
path: str, *, default_identifier_key: str
|
|
67
|
+
) -> tuple[list[str], dict[str, str]]:
|
|
68
|
+
"""Infer the template name segments and the template context for a given URI path."""
|
|
69
|
+
name_segments: list[str] = []
|
|
70
|
+
context: OrderedDict[str, str] = OrderedDict()
|
|
71
|
+
for segment in (s for s in path.split("/") if s):
|
|
72
|
+
if looks_like_id(segment):
|
|
73
|
+
if name_segments:
|
|
74
|
+
# this is a nested identifier, use the last name segment as the key
|
|
75
|
+
context[name_segments[-1]] = segment
|
|
76
|
+
else:
|
|
77
|
+
# this identifier is unscoped, use our default identifier key
|
|
78
|
+
context[default_identifier_key] = segment
|
|
79
|
+
else:
|
|
80
|
+
name_segments.append(segment)
|
|
81
|
+
|
|
82
|
+
return name_segments, context
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def iter_possible_template_filenames(
|
|
86
|
+
name_segments: list[str],
|
|
87
|
+
context: dict[str, str],
|
|
88
|
+
*,
|
|
89
|
+
template_file_separator: str,
|
|
90
|
+
template_file_extension: str,
|
|
91
|
+
default_template_name: str,
|
|
92
|
+
) -> Generator[str, None, None]:
|
|
93
|
+
"""Infer the template filename from the name segments and context.
|
|
94
|
+
|
|
95
|
+
We have a cascade of possible filename formats:
|
|
96
|
+
|
|
97
|
+
- <n>.<id>.<id>.j2
|
|
98
|
+
- <n>.<id>.j2
|
|
99
|
+
- <n>.j2
|
|
100
|
+
|
|
101
|
+
The first option is the most specific, and the last option is the least specific.
|
|
102
|
+
The IDs correspond to any identifiers found in the path of the request, in order.
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
if name_segments:
|
|
106
|
+
if context:
|
|
107
|
+
for prefix in prefixes(context.values(), reverse=True):
|
|
108
|
+
yield (
|
|
109
|
+
f"{template_file_separator.join(name_segments)}.{'.'.join(prefix)}{template_file_extension}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
yield template_file_separator.join(name_segments) + template_file_extension
|
|
113
|
+
|
|
114
|
+
yield default_template_name
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Shared fixtures for the unit-tests."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from fastapi import FastAPI
|
|
7
|
+
|
|
8
|
+
from mockstack.config import OpenTelemetrySettings, Settings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def app():
|
|
13
|
+
"""Create a FastAPI app for testing."""
|
|
14
|
+
return FastAPI()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def templates_dir():
|
|
19
|
+
"""Return the path to the test templates directory."""
|
|
20
|
+
return os.path.join(os.path.dirname(__file__), "fixtures", "templates")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def settings(templates_dir):
|
|
25
|
+
"""Return a Settings object for testing."""
|
|
26
|
+
return Settings(
|
|
27
|
+
templates_dir=templates_dir,
|
|
28
|
+
opentelemetry=OpenTelemetrySettings(enabled=False),
|
|
29
|
+
)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Tests for the catchall router module."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from fastapi.responses import JSONResponse
|
|
7
|
+
from starlette.testclient import TestClient
|
|
8
|
+
|
|
9
|
+
from mockstack.routers.catchall import catchall_router_provider
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def mock_strategy():
|
|
14
|
+
"""Create a mock strategy for testing."""
|
|
15
|
+
strategy = AsyncMock()
|
|
16
|
+
response_data = {"message": "Mock response"}
|
|
17
|
+
strategy.apply.return_value = JSONResponse(content=response_data)
|
|
18
|
+
return strategy
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.asyncio
|
|
22
|
+
async def test_catchall_router_provider(app, settings, mock_strategy):
|
|
23
|
+
"""Test that the catchall router provider sets up routes correctly."""
|
|
24
|
+
# Set up the app state with the mock strategy
|
|
25
|
+
app.state.strategy = mock_strategy
|
|
26
|
+
|
|
27
|
+
# Apply the router provider
|
|
28
|
+
catchall_router_provider(app, settings)
|
|
29
|
+
|
|
30
|
+
# Create a test client
|
|
31
|
+
client = TestClient(app)
|
|
32
|
+
|
|
33
|
+
# Test each HTTP method
|
|
34
|
+
for method in ["GET", "POST", "PUT", "DELETE", "PATCH"]:
|
|
35
|
+
# Make the request using the test client
|
|
36
|
+
response = client.request(method, "/test/path")
|
|
37
|
+
|
|
38
|
+
# Verify the response
|
|
39
|
+
assert response.status_code == 200
|
|
40
|
+
assert response.json() == {"message": "Mock response"}
|
|
41
|
+
|
|
42
|
+
# Verify the strategy was called
|
|
43
|
+
mock_strategy.apply.assert_called_once()
|
|
44
|
+
|
|
45
|
+
# Reset the mock for the next iteration
|
|
46
|
+
mock_strategy.reset_mock()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Tests for the homepage router module."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from mockstack.routers.homepage import homepage_router_provider
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.asyncio
|
|
9
|
+
async def test_homepage_router_provider(app, settings):
|
|
10
|
+
"""Test that the homepage router provider sets up routes correctly."""
|
|
11
|
+
# Apply the router provider
|
|
12
|
+
router = homepage_router_provider(app, settings)
|
|
13
|
+
|
|
14
|
+
# Include the router in the app
|
|
15
|
+
app.include_router(router)
|
|
16
|
+
|
|
17
|
+
# Create a test client
|
|
18
|
+
from fastapi.testclient import TestClient
|
|
19
|
+
|
|
20
|
+
client = TestClient(app)
|
|
21
|
+
|
|
22
|
+
# Test the homepage endpoint
|
|
23
|
+
response = client.get("/")
|
|
24
|
+
|
|
25
|
+
# Verify the response
|
|
26
|
+
assert response.status_code == 200
|
|
27
|
+
assert response.json() == {"Hello": "World"}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""Unit tests for the filefixtures strategy module."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from fastapi import HTTPException, Request
|
|
7
|
+
|
|
8
|
+
from mockstack.strategies.filefixtures import (
|
|
9
|
+
FileFixturesStrategy,
|
|
10
|
+
)
|
|
11
|
+
from mockstack.templating import (
|
|
12
|
+
iter_possible_template_arguments,
|
|
13
|
+
iter_possible_template_filenames,
|
|
14
|
+
parse_template_name_segments_and_context,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.mark.parametrize(
|
|
19
|
+
"path,expected_results",
|
|
20
|
+
[
|
|
21
|
+
(
|
|
22
|
+
"/api/v1/projects/1234",
|
|
23
|
+
[
|
|
24
|
+
{
|
|
25
|
+
"name": "api-v1-projects.1234.j2",
|
|
26
|
+
"context": {"projects": "1234"},
|
|
27
|
+
"media_type": "application/json",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "api-v1-projects.j2",
|
|
31
|
+
"context": {"projects": "1234"},
|
|
32
|
+
"media_type": "application/json",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "index.j2",
|
|
36
|
+
"context": {"projects": "1234"},
|
|
37
|
+
"media_type": "application/json",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
),
|
|
41
|
+
(
|
|
42
|
+
"/api/v1/users/3a4e5ad9-17ee-41af-972f-864dfccd4856",
|
|
43
|
+
[
|
|
44
|
+
{
|
|
45
|
+
"name": "api-v1-users.3a4e5ad9-17ee-41af-972f-864dfccd4856.j2",
|
|
46
|
+
"context": {"users": "3a4e5ad9-17ee-41af-972f-864dfccd4856"},
|
|
47
|
+
"media_type": "application/json",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "api-v1-users.j2",
|
|
51
|
+
"context": {"users": "3a4e5ad9-17ee-41af-972f-864dfccd4856"},
|
|
52
|
+
"media_type": "application/json",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "index.j2",
|
|
56
|
+
"context": {"users": "3a4e5ad9-17ee-41af-972f-864dfccd4856"},
|
|
57
|
+
"media_type": "application/json",
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
),
|
|
61
|
+
(
|
|
62
|
+
"/api/v1/projects",
|
|
63
|
+
[
|
|
64
|
+
{
|
|
65
|
+
"name": "api-v1-projects.j2",
|
|
66
|
+
"context": {},
|
|
67
|
+
"media_type": "application/json",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "index.j2",
|
|
71
|
+
"context": {},
|
|
72
|
+
"media_type": "application/json",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
),
|
|
76
|
+
(
|
|
77
|
+
"/1234",
|
|
78
|
+
[
|
|
79
|
+
{
|
|
80
|
+
"name": "index.j2",
|
|
81
|
+
"context": {"id": "1234"},
|
|
82
|
+
"media_type": "application/json",
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
),
|
|
86
|
+
],
|
|
87
|
+
)
|
|
88
|
+
def test_iter_possible_template_arguments(
|
|
89
|
+
path: str,
|
|
90
|
+
expected_results: list,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Test the iter_possible_template_arguments function with various paths."""
|
|
93
|
+
request = Request(
|
|
94
|
+
scope={
|
|
95
|
+
"type": "http",
|
|
96
|
+
"method": "GET",
|
|
97
|
+
"path": path,
|
|
98
|
+
"query_string": b"",
|
|
99
|
+
"headers": [],
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
results = list(iter_possible_template_arguments(request))
|
|
104
|
+
assert len(results) == len(expected_results)
|
|
105
|
+
|
|
106
|
+
for actual, expected in zip(results, expected_results):
|
|
107
|
+
assert actual["name"] == expected["name"]
|
|
108
|
+
assert actual["context"] == expected["context"]
|
|
109
|
+
assert actual["media_type"] == expected["media_type"]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_iter_possible_template_arguments_with_custom_media_type():
|
|
113
|
+
"""Test that custom media type from headers is respected."""
|
|
114
|
+
request = Request(
|
|
115
|
+
scope={
|
|
116
|
+
"type": "http",
|
|
117
|
+
"method": "GET",
|
|
118
|
+
"path": "/api/v1/projects",
|
|
119
|
+
"query_string": b"",
|
|
120
|
+
"headers": [(b"content-type", b"application/xml")],
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
results = list(iter_possible_template_arguments(request))
|
|
125
|
+
assert len(results) == 2
|
|
126
|
+
assert results[0]["media_type"] == "application/xml"
|
|
127
|
+
assert results[1]["media_type"] == "application/xml"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_parse_template_name_segments_and_context():
|
|
131
|
+
"""Test the parse_template_name_segments_and_context function."""
|
|
132
|
+
# Test with a simple path
|
|
133
|
+
name_segments, context = parse_template_name_segments_and_context(
|
|
134
|
+
"/api/v1/projects/1234", default_identifier_key="id"
|
|
135
|
+
)
|
|
136
|
+
assert name_segments == ["api", "v1", "projects"]
|
|
137
|
+
assert context == {"projects": "1234"}
|
|
138
|
+
|
|
139
|
+
# Test with a path with no identifiers
|
|
140
|
+
name_segments, context = parse_template_name_segments_and_context(
|
|
141
|
+
"/api/v1/projects", default_identifier_key="id"
|
|
142
|
+
)
|
|
143
|
+
assert name_segments == ["api", "v1", "projects"]
|
|
144
|
+
assert context == {}
|
|
145
|
+
|
|
146
|
+
# Test with a path with only an identifier
|
|
147
|
+
name_segments, context = parse_template_name_segments_and_context(
|
|
148
|
+
"/1234", default_identifier_key="id"
|
|
149
|
+
)
|
|
150
|
+
assert name_segments == []
|
|
151
|
+
assert context == {"id": "1234"}
|
|
152
|
+
|
|
153
|
+
# Test with a path with multiple identifiers
|
|
154
|
+
name_segments, context = parse_template_name_segments_and_context(
|
|
155
|
+
"/api/v1/projects/1234/tasks/5678", default_identifier_key="id"
|
|
156
|
+
)
|
|
157
|
+
assert name_segments == ["api", "v1", "projects", "tasks"]
|
|
158
|
+
assert context == {"projects": "1234", "tasks": "5678"}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_iter_possible_template_filenames():
|
|
162
|
+
"""Test the iter_possible_template_filenames function."""
|
|
163
|
+
# Test with name segments and context
|
|
164
|
+
filenames = list(
|
|
165
|
+
iter_possible_template_filenames(
|
|
166
|
+
["api", "v1", "projects"],
|
|
167
|
+
context={"projects": "1234"},
|
|
168
|
+
template_file_separator="-",
|
|
169
|
+
template_file_extension=".j2",
|
|
170
|
+
default_template_name="index.j2",
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
assert filenames == ["api-v1-projects.1234.j2", "api-v1-projects.j2", "index.j2"]
|
|
174
|
+
|
|
175
|
+
# Test with name segments and no context
|
|
176
|
+
filenames = list(
|
|
177
|
+
iter_possible_template_filenames(
|
|
178
|
+
["api", "v1", "projects"],
|
|
179
|
+
context={},
|
|
180
|
+
template_file_separator="-",
|
|
181
|
+
template_file_extension=".j2",
|
|
182
|
+
default_template_name="index.j2",
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
assert filenames == ["api-v1-projects.j2", "index.j2"]
|
|
186
|
+
|
|
187
|
+
# Test with no name segments and context
|
|
188
|
+
filenames = list(
|
|
189
|
+
iter_possible_template_filenames(
|
|
190
|
+
[],
|
|
191
|
+
context={"id": "1234"},
|
|
192
|
+
template_file_separator="-",
|
|
193
|
+
template_file_extension=".j2",
|
|
194
|
+
default_template_name="index.j2",
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
assert filenames == ["index.j2"]
|
|
198
|
+
|
|
199
|
+
# Test with no name segments and no context
|
|
200
|
+
filenames = list(
|
|
201
|
+
iter_possible_template_filenames(
|
|
202
|
+
[],
|
|
203
|
+
context={},
|
|
204
|
+
template_file_separator="-",
|
|
205
|
+
template_file_extension=".j2",
|
|
206
|
+
default_template_name="index.j2",
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
assert filenames == ["index.j2"]
|
|
210
|
+
|
|
211
|
+
# Test with custom separator and extension
|
|
212
|
+
filenames = list(
|
|
213
|
+
iter_possible_template_filenames(
|
|
214
|
+
["api", "v1", "projects"],
|
|
215
|
+
context={"projects": "1234"},
|
|
216
|
+
template_file_separator="_",
|
|
217
|
+
template_file_extension=".html",
|
|
218
|
+
default_template_name="default.html",
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
assert filenames == [
|
|
222
|
+
"api_v1_projects.1234.html",
|
|
223
|
+
"api_v1_projects.html",
|
|
224
|
+
"default.html",
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_filefixtures_strategy_init(settings):
|
|
229
|
+
"""Test the FileFixturesStrategy initialization."""
|
|
230
|
+
strategy = FileFixturesStrategy(settings)
|
|
231
|
+
assert strategy.templates_dir == settings.templates_dir
|
|
232
|
+
assert strategy.env is not None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@pytest.mark.asyncio
|
|
236
|
+
async def test_filefixtures_strategy_apply(settings):
|
|
237
|
+
"""Test the FileFixturesStrategy apply method."""
|
|
238
|
+
strategy = FileFixturesStrategy(settings)
|
|
239
|
+
request = Request(
|
|
240
|
+
scope={
|
|
241
|
+
"type": "http",
|
|
242
|
+
"method": "GET",
|
|
243
|
+
"path": "/api/v1/projects/1234",
|
|
244
|
+
"query_string": b"",
|
|
245
|
+
"headers": [],
|
|
246
|
+
}
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
with pytest.raises(HTTPException) as exc_info:
|
|
250
|
+
await strategy.apply(request)
|
|
251
|
+
|
|
252
|
+
assert exc_info.value.status_code == 404
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@pytest.mark.asyncio
|
|
256
|
+
async def test_file_fixtures_strategy_apply_success(settings):
|
|
257
|
+
"""Test the FileFixturesStrategy apply method when template exists."""
|
|
258
|
+
# Setup
|
|
259
|
+
strategy = FileFixturesStrategy(settings)
|
|
260
|
+
|
|
261
|
+
# Create a mock template
|
|
262
|
+
mock_template = MagicMock()
|
|
263
|
+
mock_template.render.return_value = '{"status": "success"}'
|
|
264
|
+
|
|
265
|
+
# Patch the environment to return our mock template
|
|
266
|
+
with (
|
|
267
|
+
patch.object(strategy.env, "get_template", return_value=mock_template),
|
|
268
|
+
patch("os.path.exists", return_value=True),
|
|
269
|
+
):
|
|
270
|
+
request = Request(
|
|
271
|
+
scope={
|
|
272
|
+
"type": "http",
|
|
273
|
+
"method": "GET",
|
|
274
|
+
"path": "/api/v1/projects/1234",
|
|
275
|
+
"query_string": b"",
|
|
276
|
+
"headers": [],
|
|
277
|
+
}
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Execute
|
|
281
|
+
response = await strategy.apply(request)
|
|
282
|
+
|
|
283
|
+
# Assert
|
|
284
|
+
assert response.media_type == "application/json"
|
|
285
|
+
assert response.body.decode() == '{"status": "success"}'
|
|
286
|
+
mock_template.render.assert_called_once()
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@pytest.mark.asyncio
|
|
290
|
+
async def test_file_fixtures_strategy_apply_template_not_found(settings):
|
|
291
|
+
"""Test the FileFixturesStrategy apply method when template doesn't exist."""
|
|
292
|
+
# Setup
|
|
293
|
+
strategy = FileFixturesStrategy(settings)
|
|
294
|
+
|
|
295
|
+
# Mock os.path.exists to return False for all template files
|
|
296
|
+
with patch("os.path.exists", return_value=False):
|
|
297
|
+
request = Request(
|
|
298
|
+
scope={
|
|
299
|
+
"type": "http",
|
|
300
|
+
"method": "GET",
|
|
301
|
+
"path": "/api/v1/projects/1234",
|
|
302
|
+
"query_string": b"",
|
|
303
|
+
"headers": [],
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Execute and Assert
|
|
308
|
+
with pytest.raises(HTTPException) as exc_info:
|
|
309
|
+
await strategy.apply(request)
|
|
310
|
+
|
|
311
|
+
assert exc_info.value.status_code == 404
|
|
312
|
+
assert "Template not found" in str(exc_info.value.detail)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Tests for the display module."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
from mockstack.display import ANSIColors, announce
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_ansicolors_constants():
|
|
9
|
+
"""Test that ANSIColors constants are defined correctly."""
|
|
10
|
+
assert ANSIColors.HEADER == "\033[95m"
|
|
11
|
+
assert ANSIColors.OKBLUE == "\033[94m"
|
|
12
|
+
assert ANSIColors.OKCYAN == "\033[96m"
|
|
13
|
+
assert ANSIColors.OKGREEN == "\033[92m"
|
|
14
|
+
assert ANSIColors.WARNING == "\033[93m"
|
|
15
|
+
assert ANSIColors.FAIL == "\033[91m"
|
|
16
|
+
assert ANSIColors.ENDC == "\033[0m"
|
|
17
|
+
assert ANSIColors.BOLD == "\033[1m"
|
|
18
|
+
assert ANSIColors.UNDERLINE == "\033[4m"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_announce(settings):
|
|
22
|
+
"""Test the announce function logs the correct message."""
|
|
23
|
+
with patch("mockstack.display.logging") as mock_logging:
|
|
24
|
+
mock_logger = mock_logging.getLogger.return_value
|
|
25
|
+
|
|
26
|
+
announce(settings)
|
|
27
|
+
|
|
28
|
+
mock_logging.getLogger.assert_called_once_with("uvicorn")
|
|
29
|
+
mock_logger.info.assert_called()
|
|
30
|
+
|
|
31
|
+
# Check that the log message contains the expected information
|
|
32
|
+
first_log_message = mock_logger.info.call_args[0][0]
|
|
33
|
+
assert "OpenTelemetry enabled:" in first_log_message
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Unit tests for the identifiers module."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from mockstack.identifiers import looks_like_id, prefixes
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_prefixes():
|
|
9
|
+
# Test basic functionality
|
|
10
|
+
assert list(prefixes([1, 2, 3])) == [(1,), (1, 2), (1, 2, 3)]
|
|
11
|
+
|
|
12
|
+
# Test with reverse=True
|
|
13
|
+
assert list(prefixes([1, 2, 3], reverse=True)) == [(1, 2, 3), (1, 2), (1,)]
|
|
14
|
+
|
|
15
|
+
# Test with empty list
|
|
16
|
+
assert list(prefixes([])) == []
|
|
17
|
+
|
|
18
|
+
# Test with single element
|
|
19
|
+
assert list(prefixes([1])) == [(1,)]
|
|
20
|
+
|
|
21
|
+
# Test with strings
|
|
22
|
+
assert list(prefixes(["a", "b", "c"])) == [("a",), ("a", "b"), ("a", "b", "c")]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.mark.parametrize(
|
|
26
|
+
"chunk,expected,reason",
|
|
27
|
+
[
|
|
28
|
+
# Even length numeric IDs
|
|
29
|
+
("1234", True, "Even length numeric"),
|
|
30
|
+
("12", True, "Even length numeric"),
|
|
31
|
+
("1234567890", True, "Even length numeric"),
|
|
32
|
+
# Odd length numeric IDs
|
|
33
|
+
("123", False, "Odd length numeric"),
|
|
34
|
+
("12345", False, "Odd length numeric"),
|
|
35
|
+
# Even length hexadecimal IDs
|
|
36
|
+
("abcd", True, "Even length hex"),
|
|
37
|
+
("1234abcd", True, "Even length hex"),
|
|
38
|
+
("1234567890abcdef", True, "Even length hex"),
|
|
39
|
+
# Odd length hexadecimal IDs
|
|
40
|
+
("abc", False, "Odd length hex"),
|
|
41
|
+
("1234567890abcde", False, "Odd length hex"),
|
|
42
|
+
("1234567890abcdefg", False, "Invalid hex character"),
|
|
43
|
+
("xyz", False, "Not hex"),
|
|
44
|
+
# UUID format (36 characters with dashes)
|
|
45
|
+
("3a4e5ad9-17ee-41af-972f-864dfccd4856", True, "Valid UUID"),
|
|
46
|
+
("3A4E5AD9-17EE-41AF-972F-864DFCCD4856", True, "UUID with uppercase"),
|
|
47
|
+
("3a4e5ad917ee41af972f864dfccd4856", True, "UUID without dashes"),
|
|
48
|
+
("3a4e5ad9-17ee-41af-972f-864dfccd485", False, "UUID too short"),
|
|
49
|
+
("3a4e5ad9-17ee-41af-972f-864dfccd4856-", False, "UUID too long"),
|
|
50
|
+
("3a4e5ad9-17ee-41af-972f-864dfccd485g", False, "UUID with invalid char"),
|
|
51
|
+
# Common non-ID path segments
|
|
52
|
+
("api", False, "Common path segment"),
|
|
53
|
+
("v1", False, "API version"),
|
|
54
|
+
("users", False, "Resource name"),
|
|
55
|
+
("projects", False, "Resource name"),
|
|
56
|
+
("index", False, "Page name"),
|
|
57
|
+
("create", False, "Action name"),
|
|
58
|
+
("update", False, "Action name"),
|
|
59
|
+
("delete", False, "Action name"),
|
|
60
|
+
],
|
|
61
|
+
)
|
|
62
|
+
def test_looks_like_id(chunk: str, expected: bool, reason: str) -> None:
|
|
63
|
+
"""Test the looks_like_id function with various inputs.
|
|
64
|
+
|
|
65
|
+
The test cases cover:
|
|
66
|
+
1. Even and odd length numeric IDs
|
|
67
|
+
2. Even and odd length hexadecimal IDs
|
|
68
|
+
3. Valid and invalid UUID formats
|
|
69
|
+
4. Common non-ID path segments
|
|
70
|
+
"""
|
|
71
|
+
assert looks_like_id(chunk) == expected, f"Failed for {chunk} ({reason})"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_looks_like_id_empty_string():
|
|
75
|
+
"""Test that empty string is not considered an ID."""
|
|
76
|
+
assert not looks_like_id("")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_looks_like_id_whitespace():
|
|
80
|
+
"""Test that whitespace is not considered an ID."""
|
|
81
|
+
assert not looks_like_id(" ")
|
|
82
|
+
assert not looks_like_id("\t")
|
|
83
|
+
assert not looks_like_id("\n")
|
|
84
|
+
assert not looks_like_id(" ")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_looks_like_id_special_chars():
|
|
88
|
+
"""Test that strings with special characters are not considered IDs."""
|
|
89
|
+
assert not looks_like_id("12-34") # Hyphen in wrong place
|
|
90
|
+
assert not looks_like_id("12_34") # Underscore
|
|
91
|
+
assert not looks_like_id("12.34") # Period
|
|
92
|
+
assert not looks_like_id("12/34") # Slash
|
|
93
|
+
assert not looks_like_id("12+34") # Plus
|
|
94
|
+
assert not looks_like_id("@1234") # At symbol
|