Refactor: implemented Repository Factory and Async Mutator Engine. Decoupled storage logic and enforced Dependency Injection across CLI and Web API. Updated documentation.
Some checks failed
CI / Lint (ruff) (push) Successful in 12s
CI / SAST (bandit) (push) Successful in 13s
CI / Dependency audit (pip-audit) (push) Successful in 22s
CI / Test (Standard) (3.11) (push) Failing after 54s
CI / Test (Standard) (3.12) (push) Successful in 1m35s
CI / Test (Live) (3.11) (push) Has been skipped
CI / Test (Fuzz) (3.11) (push) Has been skipped
CI / Merge dev → testing (push) Has been skipped
CI / Prepare Merge to Main (push) Has been skipped
CI / Finalize Merge to Main (push) Has been skipped
Some checks failed
CI / Lint (ruff) (push) Successful in 12s
CI / SAST (bandit) (push) Successful in 13s
CI / Dependency audit (pip-audit) (push) Successful in 22s
CI / Test (Standard) (3.11) (push) Failing after 54s
CI / Test (Standard) (3.12) (push) Successful in 1m35s
CI / Test (Live) (3.11) (push) Has been skipped
CI / Test (Fuzz) (3.11) (push) Has been skipped
CI / Merge dev → testing (push) Has been skipped
CI / Prepare Merge to Main (push) Has been skipped
CI / Finalize Merge to Main (push) Has been skipped
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
"""
|
||||
Tests for the mutate interval API endpoint.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import httpx
|
||||
from unittest.mock import patch
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, AsyncMock
|
||||
|
||||
from decnet.config import DeckyConfig, DecnetConfig
|
||||
|
||||
@@ -37,19 +35,20 @@ class TestMutateInterval:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_active_deployment(self, client: httpx.AsyncClient, auth_token: str):
|
||||
with patch("decnet.web.router.fleet.api_mutate_interval.load_state", return_value=None):
|
||||
with patch("decnet.web.router.fleet.api_mutate_interval.repo", new_callable=AsyncMock) as mock_repo:
|
||||
mock_repo.get_state.return_value = None
|
||||
resp = await client.put(
|
||||
"/api/v1/deckies/decky-01/mutate-interval",
|
||||
headers={"Authorization": f"Bearer {auth_token}"},
|
||||
json={"mutate_interval": 60},
|
||||
)
|
||||
assert resp.status_code == 500
|
||||
assert resp.status_code == 400
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_decky_not_found(self, client: httpx.AsyncClient, auth_token: str):
|
||||
config = _config()
|
||||
with patch("decnet.web.router.fleet.api_mutate_interval.load_state",
|
||||
return_value=(config, Path("test.yml"))):
|
||||
with patch("decnet.web.router.fleet.api_mutate_interval.repo", new_callable=AsyncMock) as mock_repo:
|
||||
mock_repo.get_state.return_value = {"config": config.model_dump(), "compose_path": "c.yml"}
|
||||
resp = await client.put(
|
||||
"/api/v1/deckies/nonexistent/mutate-interval",
|
||||
headers={"Authorization": f"Bearer {auth_token}"},
|
||||
@@ -60,30 +59,26 @@ class TestMutateInterval:
|
||||
@pytest.mark.asyncio
|
||||
async def test_successful_interval_update(self, client: httpx.AsyncClient, auth_token: str):
|
||||
config = _config()
|
||||
with patch("decnet.web.router.fleet.api_mutate_interval.load_state",
|
||||
return_value=(config, Path("test.yml"))):
|
||||
with patch("decnet.web.router.fleet.api_mutate_interval.save_state") as mock_save:
|
||||
resp = await client.put(
|
||||
"/api/v1/deckies/decky-01/mutate-interval",
|
||||
headers={"Authorization": f"Bearer {auth_token}"},
|
||||
json={"mutate_interval": 120},
|
||||
)
|
||||
with patch("decnet.web.router.fleet.api_mutate_interval.repo", new_callable=AsyncMock) as mock_repo:
|
||||
mock_repo.get_state.return_value = {"config": config.model_dump(), "compose_path": "c.yml"}
|
||||
resp = await client.put(
|
||||
"/api/v1/deckies/decky-01/mutate-interval",
|
||||
headers={"Authorization": f"Bearer {auth_token}"},
|
||||
json={"mutate_interval": 120},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["message"] == "Mutation interval updated"
|
||||
mock_save.assert_called_once()
|
||||
# Verify the interval was actually updated on the decky config
|
||||
assert config.deckies[0].mutate_interval == 120
|
||||
mock_repo.set_state.assert_awaited_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_null_interval_removes_mutation(self, client: httpx.AsyncClient, auth_token: str):
|
||||
config = _config()
|
||||
with patch("decnet.web.router.fleet.api_mutate_interval.load_state",
|
||||
return_value=(config, Path("test.yml"))):
|
||||
with patch("decnet.web.router.fleet.api_mutate_interval.save_state"):
|
||||
resp = await client.put(
|
||||
"/api/v1/deckies/decky-01/mutate-interval",
|
||||
headers={"Authorization": f"Bearer {auth_token}"},
|
||||
json={"mutate_interval": None},
|
||||
)
|
||||
with patch("decnet.web.router.fleet.api_mutate_interval.repo", new_callable=AsyncMock) as mock_repo:
|
||||
mock_repo.get_state.return_value = {"config": config.model_dump(), "compose_path": "c.yml"}
|
||||
resp = await client.put(
|
||||
"/api/v1/deckies/decky-01/mutate-interval",
|
||||
headers={"Authorization": f"Bearer {auth_token}"},
|
||||
json={"mutate_interval": None},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert config.deckies[0].mutate_interval is None
|
||||
mock_repo.set_state.assert_awaited_once()
|
||||
|
||||
@@ -33,11 +33,11 @@ async def test_fuzz_get_logs(client: httpx.AsyncClient, auth_token: str, limit:
|
||||
_params: dict[str, Any] = {"limit": limit, "offset": offset}
|
||||
if search is not None:
|
||||
_params["search"] = search
|
||||
|
||||
|
||||
_response: httpx.Response = await client.get(
|
||||
"/api/v1/logs",
|
||||
params=_params,
|
||||
headers={"Authorization": f"Bearer {auth_token}"}
|
||||
)
|
||||
|
||||
|
||||
assert _response.status_code in (200, 422)
|
||||
|
||||
@@ -9,13 +9,13 @@ import pytest
|
||||
from datetime import datetime, timedelta
|
||||
from freezegun import freeze_time
|
||||
from hypothesis import given, settings, strategies as st
|
||||
from decnet.web.db.sqlite.repository import SQLiteRepository
|
||||
from decnet.web.db.factory import get_repository
|
||||
from ..conftest import _FUZZ_SETTINGS
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def repo(tmp_path):
|
||||
return SQLiteRepository(db_path=str(tmp_path / "histogram_test.db"))
|
||||
return get_repository(db_path=str(tmp_path / "histogram_test.db"))
|
||||
|
||||
|
||||
def _log(decky="d", service="ssh", ip="1.2.3.4", timestamp=None):
|
||||
|
||||
@@ -21,7 +21,7 @@ class TestStreamEvents:
|
||||
# We force the generator to exit immediately by making the first awaitable raise
|
||||
with patch("decnet.web.router.stream.api_stream_events.repo") as mock_repo:
|
||||
mock_repo.get_max_log_id = AsyncMock(side_effect=StopAsyncIteration)
|
||||
|
||||
|
||||
# This will hit the 'except Exception' or just exit the generator
|
||||
resp = await client.get(
|
||||
"/api/v1/stream",
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
"""
|
||||
Direct async tests for SQLiteRepository.
|
||||
These exercise the DB layer without going through the HTTP stack,
|
||||
covering DEBT-006 (zero test coverage on the database layer).
|
||||
Direct async tests for the configured Repository implementation.
|
||||
These exercise the DB layer without going through the HTTP stack.
|
||||
"""
|
||||
import json
|
||||
import pytest
|
||||
from hypothesis import given, settings, strategies as st
|
||||
from decnet.web.db.sqlite.repository import SQLiteRepository
|
||||
from decnet.web.db.factory import get_repository
|
||||
from .conftest import _FUZZ_SETTINGS
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def repo(tmp_path):
|
||||
return SQLiteRepository(db_path=str(tmp_path / "test.db"))
|
||||
return get_repository(db_path=str(tmp_path / "test.db"))
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
||||
@@ -11,16 +11,82 @@ replace the checks list with the default (remove the argument) for full complian
|
||||
Requires DECNET_DEVELOPER=true (set in tests/conftest.py) to expose /openapi.json.
|
||||
"""
|
||||
import pytest
|
||||
import schemathesis
|
||||
from hypothesis import settings
|
||||
from schemathesis.checks import not_a_server_error
|
||||
from decnet.web.api import app
|
||||
import schemathesis as st
|
||||
from hypothesis import settings, Verbosity
|
||||
from decnet.web.auth import create_access_token
|
||||
|
||||
schema = schemathesis.openapi.from_asgi("/openapi.json", app)
|
||||
import subprocess
|
||||
import socket
|
||||
import sys
|
||||
import atexit
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# Configuration for the automated live server
|
||||
LIVE_PORT = 8008
|
||||
LIVE_SERVER_URL = f"http://127.0.0.1:{LIVE_PORT}"
|
||||
TEST_SECRET = "test-secret-for-automated-fuzzing"
|
||||
|
||||
# Standardize the secret for the test process too so tokens can be verified
|
||||
import decnet.web.auth
|
||||
decnet.web.auth.SECRET_KEY = TEST_SECRET
|
||||
|
||||
# Create a valid token for an admin-like user
|
||||
TEST_TOKEN = create_access_token({"uuid": "00000000-0000-0000-0000-000000000001"})
|
||||
|
||||
@st.hook
|
||||
def before_call(context, case, *args):
|
||||
# Logged-in admin for all requests
|
||||
case.headers = case.headers or {}
|
||||
case.headers["Authorization"] = f"Bearer {TEST_TOKEN}"
|
||||
|
||||
def wait_for_port(port, timeout=10):
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
if sock.connect_ex(('127.0.0.1', port)) == 0:
|
||||
return True
|
||||
time.sleep(0.2)
|
||||
return False
|
||||
|
||||
def start_automated_server():
|
||||
# Use the current venv's uvicorn
|
||||
uvicorn_bin = "uvicorn" if os.name != "nt" else "uvicorn.exe"
|
||||
uvicorn_path = str(Path(sys.executable).parent / uvicorn_bin)
|
||||
|
||||
# Force developer and contract test modes for the sub-process
|
||||
env = os.environ.copy()
|
||||
env["DECNET_DEVELOPER"] = "true"
|
||||
env["DECNET_CONTRACT_TEST"] = "true"
|
||||
env["DECNET_JWT_SECRET"] = TEST_SECRET
|
||||
|
||||
proc = subprocess.Popen(
|
||||
[uvicorn_path, "decnet.web.api:app", "--host", "127.0.0.1", "--port", str(LIVE_PORT), "--log-level", "error"],
|
||||
env=env,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
|
||||
# Register cleanup
|
||||
atexit.register(proc.terminate)
|
||||
|
||||
if not wait_for_port(LIVE_PORT):
|
||||
proc.terminate()
|
||||
raise RuntimeError(f"Automated server failed to start on port {LIVE_PORT}")
|
||||
|
||||
return proc
|
||||
|
||||
# Stir up the server!
|
||||
_server_proc = start_automated_server()
|
||||
|
||||
# Now Schemathesis can pull the schema from the real network port
|
||||
schema = st.openapi.from_url(f"{LIVE_SERVER_URL}/openapi.json")
|
||||
|
||||
@pytest.mark.fuzz
|
||||
@schemathesis.pytest.parametrize(api=schema)
|
||||
@settings(max_examples=5, deadline=None)
|
||||
@st.pytest.parametrize(api=schema)
|
||||
@settings(max_examples=3000, deadline=None, verbosity=Verbosity.debug)
|
||||
def test_schema_compliance(case):
|
||||
case.call_and_validate(checks=[not_a_server_error])
|
||||
#print(f"\n[Fuzzing] {case.method} {case.path} with query={case.query}")
|
||||
case.call_and_validate()
|
||||
#print(f" └─ Success")
|
||||
|
||||
Reference in New Issue
Block a user