249 lines
7.8 KiB
Python
249 lines
7.8 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from pydantic import ValidationError
|
||
|
|
|
||
|
|
from cmvr_edge_ai.config import load_config_data
|
||
|
|
|
||
|
|
|
||
|
|
def _pipeline(*, enabled: bool = True) -> dict[str, object]:
|
||
|
|
return {
|
||
|
|
"enabled": enabled,
|
||
|
|
"nodes": {"source": {"uses": "test.source@1"}},
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def test_server_config_maps_namespaced_categories_to_enabled_pipelines() -> None:
|
||
|
|
config = load_config_data(
|
||
|
|
{
|
||
|
|
"api_version": "cmvr.edge.ai/v1",
|
||
|
|
"pipelines": {"mobile_phone_remote": _pipeline()},
|
||
|
|
"server": {
|
||
|
|
"enabled": True,
|
||
|
|
"http": {
|
||
|
|
"bind": "0.0.0.0",
|
||
|
|
"port": 18080,
|
||
|
|
"bearer_token": "deployment-secret",
|
||
|
|
"allow_insecure_remote": True,
|
||
|
|
"max_request_bytes": 2_000_000,
|
||
|
|
"max_image_bytes": 1_000_000,
|
||
|
|
},
|
||
|
|
"routes": {
|
||
|
|
"detect.mobile_phone": {
|
||
|
|
"pipeline": "mobile_phone_remote",
|
||
|
|
"model_id": "yolov8n-mobile-phone@1",
|
||
|
|
"queue_capacity": 3,
|
||
|
|
}
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
assert config.server is not None
|
||
|
|
assert config.server.http.bind == "0.0.0.0"
|
||
|
|
assert config.server.http.port == 18080
|
||
|
|
assert config.server.http.bearer_token is not None
|
||
|
|
assert config.server.http.bearer_token.get_secret_value() == "deployment-secret"
|
||
|
|
assert config.server.http.model_dump(mode="json")["bearer_token"] == "**********"
|
||
|
|
assert config.server.http.allow_insecure_remote is True
|
||
|
|
route = config.server.routes["detect.mobile_phone"]
|
||
|
|
assert route.pipeline == "mobile_phone_remote"
|
||
|
|
assert route.model_id == "yolov8n-mobile-phone@1"
|
||
|
|
assert route.queue_capacity == 3
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize(
|
||
|
|
("server", "message"),
|
||
|
|
[
|
||
|
|
(
|
||
|
|
{"enabled": True, "routes": {}},
|
||
|
|
"enabled server must configure at least one route",
|
||
|
|
),
|
||
|
|
(
|
||
|
|
{
|
||
|
|
"enabled": True,
|
||
|
|
"routes": {
|
||
|
|
"detect": {
|
||
|
|
"pipeline": "remote",
|
||
|
|
"model_id": "model@1",
|
||
|
|
}
|
||
|
|
},
|
||
|
|
},
|
||
|
|
"invalid invocation category",
|
||
|
|
),
|
||
|
|
(
|
||
|
|
{
|
||
|
|
"enabled": True,
|
||
|
|
"http": {
|
||
|
|
"max_request_bytes": 1024,
|
||
|
|
"max_image_bytes": 2048,
|
||
|
|
},
|
||
|
|
"routes": {
|
||
|
|
"detect.remote": {
|
||
|
|
"pipeline": "remote",
|
||
|
|
"model_id": "model@1",
|
||
|
|
}
|
||
|
|
},
|
||
|
|
},
|
||
|
|
"max_image_bytes must not exceed max_request_bytes",
|
||
|
|
),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
def test_server_config_rejects_ambiguous_or_unsafe_values(
|
||
|
|
server: dict[str, object], message: str
|
||
|
|
) -> None:
|
||
|
|
with pytest.raises(ValidationError, match=message):
|
||
|
|
load_config_data(
|
||
|
|
{
|
||
|
|
"api_version": "cmvr.edge.ai/v1",
|
||
|
|
"pipelines": {"remote": _pipeline()},
|
||
|
|
"server": server,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_server_routes_must_reference_one_unique_enabled_pipeline() -> None:
|
||
|
|
with pytest.raises(ValidationError, match="only one public category"):
|
||
|
|
load_config_data(
|
||
|
|
{
|
||
|
|
"api_version": "cmvr.edge.ai/v1",
|
||
|
|
"pipelines": {"remote": _pipeline()},
|
||
|
|
"server": {
|
||
|
|
"enabled": True,
|
||
|
|
"routes": {
|
||
|
|
"detect.first": {
|
||
|
|
"pipeline": "remote",
|
||
|
|
"model_id": "first@1",
|
||
|
|
},
|
||
|
|
"detect.second": {
|
||
|
|
"pipeline": "remote",
|
||
|
|
"model_id": "second@1",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
with pytest.raises(ValidationError, match="disabled pipeline"):
|
||
|
|
load_config_data(
|
||
|
|
{
|
||
|
|
"api_version": "cmvr.edge.ai/v1",
|
||
|
|
"pipelines": {"remote": _pipeline(enabled=False)},
|
||
|
|
"server": {
|
||
|
|
"enabled": True,
|
||
|
|
"routes": {
|
||
|
|
"detect.remote": {
|
||
|
|
"pipeline": "remote",
|
||
|
|
"model_id": "model@1",
|
||
|
|
}
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
with pytest.raises(ValidationError, match="unknown pipeline"):
|
||
|
|
load_config_data(
|
||
|
|
{
|
||
|
|
"api_version": "cmvr.edge.ai/v1",
|
||
|
|
"pipelines": {"active": _pipeline()},
|
||
|
|
"server": {
|
||
|
|
"enabled": True,
|
||
|
|
"routes": {
|
||
|
|
"detect.remote": {
|
||
|
|
"pipeline": "missing",
|
||
|
|
"model_id": "model@1",
|
||
|
|
}
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize(
|
||
|
|
("http", "message"),
|
||
|
|
[
|
||
|
|
(
|
||
|
|
{"bind": "0.0.0.0"},
|
||
|
|
"non-loopback server.http.bind requires bearer_token",
|
||
|
|
),
|
||
|
|
(
|
||
|
|
{"bind": "edge-host", "bearer_token": "secret"},
|
||
|
|
"requires TLS or allow_insecure_remote=true",
|
||
|
|
),
|
||
|
|
(
|
||
|
|
{"tls_certfile": "/certs/server.crt"},
|
||
|
|
"tls_certfile and tls_keyfile must be configured together",
|
||
|
|
),
|
||
|
|
(
|
||
|
|
{"bearer_token": "contains whitespace"},
|
||
|
|
"bearer_token must be a non-empty token without whitespace",
|
||
|
|
),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
def test_server_http_rejects_incomplete_remote_security(
|
||
|
|
http: dict[str, object], message: str
|
||
|
|
) -> None:
|
||
|
|
with pytest.raises(ValidationError, match=message):
|
||
|
|
load_config_data(
|
||
|
|
{
|
||
|
|
"api_version": "cmvr.edge.ai/v1",
|
||
|
|
"pipelines": {"remote": _pipeline()},
|
||
|
|
"server": {
|
||
|
|
"enabled": True,
|
||
|
|
"http": http,
|
||
|
|
"routes": {
|
||
|
|
"detect.remote": {
|
||
|
|
"pipeline": "remote",
|
||
|
|
"model_id": "model@1",
|
||
|
|
}
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_server_http_accepts_loopback_without_auth_and_remote_with_tls() -> None:
|
||
|
|
loopback = load_config_data(
|
||
|
|
{
|
||
|
|
"api_version": "cmvr.edge.ai/v1",
|
||
|
|
"pipelines": {"remote": _pipeline()},
|
||
|
|
"server": {
|
||
|
|
"enabled": True,
|
||
|
|
"http": {"bind": "::1"},
|
||
|
|
"routes": {
|
||
|
|
"detect.remote": {
|
||
|
|
"pipeline": "remote",
|
||
|
|
"model_id": "model@1",
|
||
|
|
}
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
)
|
||
|
|
assert loopback.server is not None
|
||
|
|
assert loopback.server.http.bearer_token is None
|
||
|
|
|
||
|
|
secured = load_config_data(
|
||
|
|
{
|
||
|
|
"api_version": "cmvr.edge.ai/v1",
|
||
|
|
"pipelines": {"remote": _pipeline()},
|
||
|
|
"server": {
|
||
|
|
"enabled": True,
|
||
|
|
"http": {
|
||
|
|
"bind": "192.0.2.10",
|
||
|
|
"bearer_token": "deployment-secret",
|
||
|
|
"tls_certfile": "/certs/server.crt",
|
||
|
|
"tls_keyfile": "/certs/server.key",
|
||
|
|
},
|
||
|
|
"routes": {
|
||
|
|
"detect.remote": {
|
||
|
|
"pipeline": "remote",
|
||
|
|
"model_id": "model@1",
|
||
|
|
}
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
)
|
||
|
|
assert secured.server is not None
|
||
|
|
assert str(secured.server.http.tls_certfile) == "/certs/server.crt"
|
||
|
|
assert str(secured.server.http.tls_keyfile) == "/certs/server.key"
|