piphi-runtime-testkit-python 0.1.1__tar.gz → 0.1.2__tar.gz
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.
- {piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/PKG-INFO +1 -1
- {piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/pyproject.toml +2 -2
- {piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/src/piphi_runtime_testkit_python/__init__.py +9 -2
- {piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/src/piphi_runtime_testkit_python/builders.py +39 -0
- {piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/src/piphi_runtime_testkit_python/mock_core.py +37 -0
- {piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/tests/test_mock_core.py +54 -1
- piphi_runtime_testkit_python-0.1.2/tests/test_runtime_config_contract.py +56 -0
- {piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/LICENSE +0 -0
- {piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/README.md +0 -0
- {piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/src/piphi_runtime_testkit_python/assertions.py +0 -0
- {piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/src/piphi_runtime_testkit_python/pytest_plugin.py +0 -0
- {piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/tests/__init__.py +0 -0
- {piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/tests/test_assertions.py +0 -0
- {piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/tests/test_builders.py +0 -0
- {piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/tests/test_fastapi_runtime_example.py +0 -0
- {piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/tests/test_pytest_plugin.py +0 -0
- {piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/tests/test_runtime_flow.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "piphi-runtime-testkit-python"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
description = "Pytest-first helpers for testing PiPhi runtime integrations"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "KelvinSan", email = "support@piphi.network" },
|
|
@@ -55,7 +55,7 @@ release-version = "python scripts/release.py"
|
|
|
55
55
|
dev = [
|
|
56
56
|
"fastapi>=0.115,<1.0",
|
|
57
57
|
"httpx>=0.27,<1.0",
|
|
58
|
-
"piphi-runtime-kit-python[fastapi]==0.4.
|
|
58
|
+
"piphi-runtime-kit-python[fastapi]==0.4.5",
|
|
59
59
|
"pytest>=8.3,<9.0",
|
|
60
60
|
"pytest-asyncio>=0.24,<1.0",
|
|
61
61
|
"twine>=6.2.0",
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
"""Pytest-first helpers for testing PiPhi runtime integrations."""
|
|
2
2
|
|
|
3
3
|
from .assertions import assert_entities_response, assert_event_sent, assert_telemetry_sent
|
|
4
|
-
from .builders import
|
|
5
|
-
|
|
4
|
+
from .builders import (
|
|
5
|
+
build_config_payload,
|
|
6
|
+
build_config_snapshot,
|
|
7
|
+
build_core_config_row,
|
|
8
|
+
build_runtime_headers,
|
|
9
|
+
)
|
|
10
|
+
from .mock_core import CapturedRequest, MockCoreServer, RUNTIME_CONFIG_FETCH_PATH
|
|
6
11
|
|
|
7
12
|
__all__ = [
|
|
8
13
|
"CapturedRequest",
|
|
9
14
|
"MockCoreServer",
|
|
15
|
+
"RUNTIME_CONFIG_FETCH_PATH",
|
|
10
16
|
"assert_entities_response",
|
|
11
17
|
"assert_event_sent",
|
|
12
18
|
"assert_telemetry_sent",
|
|
13
19
|
"build_config_payload",
|
|
14
20
|
"build_config_snapshot",
|
|
21
|
+
"build_core_config_row",
|
|
15
22
|
"build_runtime_headers",
|
|
16
23
|
]
|
|
@@ -50,16 +50,55 @@ def build_config_snapshot(
|
|
|
50
50
|
container_id: str = "test-container",
|
|
51
51
|
integration_id: str = "test-integration",
|
|
52
52
|
generation: int = 1,
|
|
53
|
+
deleted_config_ids: list[str] | None = None,
|
|
54
|
+
config_hash: str | None = None,
|
|
55
|
+
schema_version: int = 1,
|
|
56
|
+
updated_at: str | None = None,
|
|
53
57
|
extra: dict[str, Any] | None = None,
|
|
54
58
|
) -> dict[str, Any]:
|
|
55
59
|
"""Build a snapshot payload for `/config/sync` style tests."""
|
|
56
60
|
|
|
57
61
|
payload: dict[str, Any] = {
|
|
62
|
+
"schema_version": schema_version,
|
|
58
63
|
"container_id": container_id,
|
|
59
64
|
"integration_id": integration_id,
|
|
60
65
|
"generation": generation,
|
|
61
66
|
"configs": configs or [],
|
|
67
|
+
"deleted_config_ids": deleted_config_ids or [],
|
|
62
68
|
}
|
|
69
|
+
if config_hash is not None:
|
|
70
|
+
payload["config_hash"] = config_hash
|
|
71
|
+
if updated_at is not None:
|
|
72
|
+
payload["updated_at"] = updated_at
|
|
63
73
|
if extra:
|
|
64
74
|
payload.update(extra)
|
|
65
75
|
return payload
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def build_core_config_row(
|
|
79
|
+
*,
|
|
80
|
+
config_data: dict[str, Any] | None = None,
|
|
81
|
+
config_id: str = "config-1",
|
|
82
|
+
container_id: str = "test-container",
|
|
83
|
+
integration_id: str = "test-integration",
|
|
84
|
+
generation: int = 1,
|
|
85
|
+
extra: dict[str, Any] | None = None,
|
|
86
|
+
) -> dict[str, Any]:
|
|
87
|
+
"""Build a Core database-shaped config row for startup rehydrate tests."""
|
|
88
|
+
|
|
89
|
+
payload = build_config_payload(
|
|
90
|
+
config_id=config_id,
|
|
91
|
+
container_id=container_id,
|
|
92
|
+
integration_id=integration_id,
|
|
93
|
+
extra=config_data,
|
|
94
|
+
)
|
|
95
|
+
row: dict[str, Any] = {
|
|
96
|
+
"id": config_id,
|
|
97
|
+
"container_id": container_id,
|
|
98
|
+
"integration_id": integration_id,
|
|
99
|
+
"generation": generation,
|
|
100
|
+
"config_data": payload,
|
|
101
|
+
}
|
|
102
|
+
if extra:
|
|
103
|
+
row.update(extra)
|
|
104
|
+
return row
|
|
@@ -9,6 +9,7 @@ from typing import Any
|
|
|
9
9
|
|
|
10
10
|
TELEMETRY_PATH = "/api/v2/integrations/telemetry"
|
|
11
11
|
EVENT_PATH = "/api/v2/events/ingest"
|
|
12
|
+
RUNTIME_CONFIG_FETCH_PATH = "/api/v2/integrations/config/fetch/all/by/container/internal"
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
@dataclass(slots=True)
|
|
@@ -24,23 +25,48 @@ class _RequestStore:
|
|
|
24
25
|
def __init__(self) -> None:
|
|
25
26
|
self.telemetry_requests: list[CapturedRequest] = []
|
|
26
27
|
self.event_requests: list[CapturedRequest] = []
|
|
28
|
+
self.runtime_config_requests: list[CapturedRequest] = []
|
|
27
29
|
self.telemetry_status = 200
|
|
28
30
|
self.event_status = 200
|
|
31
|
+
self.runtime_config_status = 200
|
|
29
32
|
self.telemetry_json: Any = {"ok": True}
|
|
30
33
|
self.event_json: Any = {"ok": True}
|
|
34
|
+
self.runtime_config_json: Any = []
|
|
31
35
|
|
|
32
36
|
def reset(self) -> None:
|
|
33
37
|
self.telemetry_requests.clear()
|
|
34
38
|
self.event_requests.clear()
|
|
39
|
+
self.runtime_config_requests.clear()
|
|
35
40
|
self.telemetry_status = 200
|
|
36
41
|
self.event_status = 200
|
|
42
|
+
self.runtime_config_status = 200
|
|
37
43
|
self.telemetry_json = {"ok": True}
|
|
38
44
|
self.event_json = {"ok": True}
|
|
45
|
+
self.runtime_config_json = []
|
|
39
46
|
|
|
40
47
|
|
|
41
48
|
class _MockCoreRequestHandler(BaseHTTPRequestHandler):
|
|
42
49
|
store: _RequestStore
|
|
43
50
|
|
|
51
|
+
def do_GET(self) -> None: # noqa: N802
|
|
52
|
+
request = CapturedRequest(
|
|
53
|
+
method="GET",
|
|
54
|
+
path=self.path,
|
|
55
|
+
headers={key: value for key, value in self.headers.items()},
|
|
56
|
+
body=b"",
|
|
57
|
+
json_body=None,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if self.path.split("?", 1)[0] == RUNTIME_CONFIG_FETCH_PATH:
|
|
61
|
+
self.store.runtime_config_requests.append(request)
|
|
62
|
+
self._send_json(
|
|
63
|
+
self.store.runtime_config_status,
|
|
64
|
+
self.store.runtime_config_json,
|
|
65
|
+
)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
self._send_json(404, {"ok": False, "detail": "unknown path"})
|
|
69
|
+
|
|
44
70
|
def do_POST(self) -> None: # noqa: N802
|
|
45
71
|
content_length = int(self.headers.get("Content-Length", "0"))
|
|
46
72
|
body = self.rfile.read(content_length)
|
|
@@ -119,6 +145,10 @@ class MockCoreServer:
|
|
|
119
145
|
def event_requests(self) -> list[CapturedRequest]:
|
|
120
146
|
return self._store.event_requests
|
|
121
147
|
|
|
148
|
+
@property
|
|
149
|
+
def runtime_config_requests(self) -> list[CapturedRequest]:
|
|
150
|
+
return self._store.runtime_config_requests
|
|
151
|
+
|
|
122
152
|
def set_telemetry_response(self, *, status_code: int = 200, json_body: Any = None) -> None:
|
|
123
153
|
self._store.telemetry_status = status_code
|
|
124
154
|
self._store.telemetry_json = {"ok": status_code < 400} if json_body is None else json_body
|
|
@@ -127,6 +157,13 @@ class MockCoreServer:
|
|
|
127
157
|
self._store.event_status = status_code
|
|
128
158
|
self._store.event_json = {"ok": status_code < 400} if json_body is None else json_body
|
|
129
159
|
|
|
160
|
+
def set_runtime_config_response(self, *, status_code: int = 200, json_body: Any = None) -> None:
|
|
161
|
+
self._store.runtime_config_status = status_code
|
|
162
|
+
self._store.runtime_config_json = [] if json_body is None else json_body
|
|
163
|
+
|
|
164
|
+
def set_runtime_config_rows(self, rows: list[dict[str, Any]]) -> None:
|
|
165
|
+
self.set_runtime_config_response(status_code=200, json_body=rows)
|
|
166
|
+
|
|
130
167
|
def reset(self) -> None:
|
|
131
168
|
self._store.reset()
|
|
132
169
|
|
{piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/tests/test_mock_core.py
RENAMED
|
@@ -4,7 +4,13 @@ import json
|
|
|
4
4
|
from urllib import error
|
|
5
5
|
from urllib import request
|
|
6
6
|
|
|
7
|
-
from piphi_runtime_testkit_python
|
|
7
|
+
from piphi_runtime_testkit_python import build_core_config_row, build_runtime_headers
|
|
8
|
+
from piphi_runtime_testkit_python.mock_core import (
|
|
9
|
+
EVENT_PATH,
|
|
10
|
+
RUNTIME_CONFIG_FETCH_PATH,
|
|
11
|
+
TELEMETRY_PATH,
|
|
12
|
+
MockCoreServer,
|
|
13
|
+
)
|
|
8
14
|
|
|
9
15
|
|
|
10
16
|
def _post_json(url: str, payload: dict) -> tuple[int, dict]:
|
|
@@ -19,6 +25,16 @@ def _post_json(url: str, payload: dict) -> tuple[int, dict]:
|
|
|
19
25
|
return response.status, json.loads(response.read().decode("utf-8"))
|
|
20
26
|
|
|
21
27
|
|
|
28
|
+
def _get_json(url: str, headers: dict[str, str] | None = None) -> tuple[int, list]:
|
|
29
|
+
req = request.Request(
|
|
30
|
+
url,
|
|
31
|
+
headers=headers or {},
|
|
32
|
+
method="GET",
|
|
33
|
+
)
|
|
34
|
+
with request.urlopen(req) as response: # noqa: S310
|
|
35
|
+
return response.status, json.loads(response.read().decode("utf-8"))
|
|
36
|
+
|
|
37
|
+
|
|
22
38
|
def test_mock_core_captures_telemetry_and_events():
|
|
23
39
|
mock_core = MockCoreServer()
|
|
24
40
|
try:
|
|
@@ -170,3 +186,40 @@ def test_mock_core_invalid_json_is_captured_with_none_json_body():
|
|
|
170
186
|
assert mock_core.telemetry_requests[0].body == b"{invalid-json"
|
|
171
187
|
finally:
|
|
172
188
|
mock_core.shutdown()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_mock_core_serves_runtime_config_rows_and_captures_auth_headers():
|
|
192
|
+
mock_core = MockCoreServer()
|
|
193
|
+
try:
|
|
194
|
+
mock_core.set_runtime_config_rows(
|
|
195
|
+
[
|
|
196
|
+
build_core_config_row(
|
|
197
|
+
config_id="config-1",
|
|
198
|
+
container_id="runtime-123",
|
|
199
|
+
integration_id="test-integration",
|
|
200
|
+
config_data={"host": "10.0.0.10"},
|
|
201
|
+
)
|
|
202
|
+
]
|
|
203
|
+
)
|
|
204
|
+
headers = build_runtime_headers(
|
|
205
|
+
container_id="runtime-123",
|
|
206
|
+
internal_token="secret-token",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
status, rows = _get_json(
|
|
210
|
+
f"{mock_core.base_url}{RUNTIME_CONFIG_FETCH_PATH}?container_id=runtime-123",
|
|
211
|
+
headers=headers,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
assert status == 200
|
|
215
|
+
assert rows[0]["config_data"]["id"] == "config-1"
|
|
216
|
+
assert rows[0]["config_data"]["host"] == "10.0.0.10"
|
|
217
|
+
assert len(mock_core.runtime_config_requests) == 1
|
|
218
|
+
captured_headers = {
|
|
219
|
+
key.lower(): value
|
|
220
|
+
for key, value in mock_core.runtime_config_requests[0].headers.items()
|
|
221
|
+
}
|
|
222
|
+
assert captured_headers["x-container-id"] == "runtime-123"
|
|
223
|
+
assert captured_headers["x-piphi-integration-token"] == "secret-token"
|
|
224
|
+
finally:
|
|
225
|
+
mock_core.shutdown()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from piphi_runtime_kit_python import (
|
|
7
|
+
RuntimeConfigSnapshot,
|
|
8
|
+
RuntimeContext,
|
|
9
|
+
rehydrate_runtime_configs,
|
|
10
|
+
)
|
|
11
|
+
from piphi_runtime_testkit_python import (
|
|
12
|
+
build_core_config_row,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.asyncio
|
|
17
|
+
async def test_mock_core_supports_sdk_startup_rehydrate_contract(mock_core) -> None:
|
|
18
|
+
mock_core.set_runtime_config_rows(
|
|
19
|
+
[
|
|
20
|
+
build_core_config_row(
|
|
21
|
+
config_id="sensor-1",
|
|
22
|
+
container_id="runtime-123",
|
|
23
|
+
integration_id="test-integration",
|
|
24
|
+
config_data={"device_id": "sensor-1", "host": "10.0.0.42"},
|
|
25
|
+
)
|
|
26
|
+
]
|
|
27
|
+
)
|
|
28
|
+
runtime = RuntimeContext()
|
|
29
|
+
runtime.auth.update(container_id="runtime-123", internal_token="secret-token")
|
|
30
|
+
applied_snapshots: list[RuntimeConfigSnapshot] = []
|
|
31
|
+
|
|
32
|
+
async def apply_snapshot(snapshot: RuntimeConfigSnapshot) -> None:
|
|
33
|
+
applied_snapshots.append(snapshot)
|
|
34
|
+
|
|
35
|
+
async with httpx.AsyncClient() as client:
|
|
36
|
+
result = await rehydrate_runtime_configs(
|
|
37
|
+
runtime_context=runtime,
|
|
38
|
+
client=client,
|
|
39
|
+
apply_snapshot=apply_snapshot,
|
|
40
|
+
core_base_url=mock_core.base_url,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
assert result.core_applied is True
|
|
44
|
+
assert result.core_config_count == 1
|
|
45
|
+
assert applied_snapshots[0].container_id == "runtime-123"
|
|
46
|
+
assert applied_snapshots[0].configs[0].id == "sensor-1"
|
|
47
|
+
assert applied_snapshots[0].configs[0].device_id == "sensor-1"
|
|
48
|
+
|
|
49
|
+
captured_request = mock_core.runtime_config_requests[0]
|
|
50
|
+
captured_headers = {
|
|
51
|
+
key.lower(): value
|
|
52
|
+
for key, value in captured_request.headers.items()
|
|
53
|
+
}
|
|
54
|
+
assert "container_id=runtime-123" in captured_request.path
|
|
55
|
+
assert captured_headers["x-container-id"] == "runtime-123"
|
|
56
|
+
assert captured_headers["x-piphi-integration-token"] == "secret-token"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/tests/test_assertions.py
RENAMED
|
File without changes
|
{piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/tests/test_builders.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{piphi_runtime_testkit_python-0.1.1 → piphi_runtime_testkit_python-0.1.2}/tests/test_runtime_flow.py
RENAMED
|
File without changes
|