fix: add missing __init__.py to tests/api subpackages to fix relative imports
This commit is contained in:
0
tests/api/__init__.py
Normal file
0
tests/api/__init__.py
Normal file
0
tests/api/auth/__init__.py
Normal file
0
tests/api/auth/__init__.py
Normal file
61
tests/api/auth/test_change_pass.py
Normal file
61
tests/api/auth/test_change_pass.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import json
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from decnet.web.api import app
|
||||||
|
from hypothesis import given, strategies as st, settings
|
||||||
|
import httpx
|
||||||
|
from decnet.env import DECNET_ADMIN_USER, DECNET_ADMIN_PASSWORD
|
||||||
|
from ..conftest import _FUZZ_SETTINGS
|
||||||
|
|
||||||
|
def test_change_password() -> None:
|
||||||
|
with TestClient(app) as client:
|
||||||
|
# First login to get token
|
||||||
|
login_resp = client.post("/api/v1/auth/login", json={"username": DECNET_ADMIN_USER, "password": DECNET_ADMIN_PASSWORD})
|
||||||
|
token = login_resp.json()["access_token"]
|
||||||
|
|
||||||
|
# Try changing password with wrong old password
|
||||||
|
resp1 = client.post(
|
||||||
|
"/api/v1/auth/change-password",
|
||||||
|
json={"old_password": "wrong", "new_password": "new_secure_password"},
|
||||||
|
headers={"Authorization": f"Bearer {token}"}
|
||||||
|
)
|
||||||
|
assert resp1.status_code == 401
|
||||||
|
|
||||||
|
# Change password successfully
|
||||||
|
resp2 = client.post(
|
||||||
|
"/api/v1/auth/change-password",
|
||||||
|
json={"old_password": DECNET_ADMIN_PASSWORD, "new_password": "new_secure_password"},
|
||||||
|
headers={"Authorization": f"Bearer {token}"}
|
||||||
|
)
|
||||||
|
assert resp2.status_code == 200
|
||||||
|
|
||||||
|
# Verify old password no longer works
|
||||||
|
resp3 = client.post("/api/v1/auth/login", json={"username": DECNET_ADMIN_USER, "password": DECNET_ADMIN_PASSWORD})
|
||||||
|
assert resp3.status_code == 401
|
||||||
|
|
||||||
|
# Verify new password works and must_change_password is False
|
||||||
|
resp4 = client.post("/api/v1/auth/login", json={"username": DECNET_ADMIN_USER, "password": "new_secure_password"})
|
||||||
|
assert resp4.status_code == 200
|
||||||
|
assert resp4.json()["must_change_password"] is False
|
||||||
|
|
||||||
|
@settings(**_FUZZ_SETTINGS)
|
||||||
|
@given(
|
||||||
|
old_password=st.text(min_size=0, max_size=2048),
|
||||||
|
new_password=st.text(min_size=0, max_size=2048)
|
||||||
|
)
|
||||||
|
def test_fuzz_change_password(old_password: str, new_password: str) -> None:
|
||||||
|
"""Fuzz the change-password endpoint with random strings."""
|
||||||
|
with TestClient(app) as _client:
|
||||||
|
# Get valid token first
|
||||||
|
_login_resp: httpx.Response = _client.post("/api/v1/auth/login", json={"username": DECNET_ADMIN_USER, "password": DECNET_ADMIN_PASSWORD})
|
||||||
|
_token: str = _login_resp.json()["access_token"]
|
||||||
|
|
||||||
|
_payload: dict[str, str] = {"old_password": old_password, "new_password": new_password}
|
||||||
|
try:
|
||||||
|
_response: httpx.Response = _client.post(
|
||||||
|
"/api/v1/auth/change-password",
|
||||||
|
json=_payload,
|
||||||
|
headers={"Authorization": f"Bearer {_token}"}
|
||||||
|
)
|
||||||
|
assert _response.status_code in (200, 401, 422)
|
||||||
|
except (UnicodeEncodeError, json.JSONDecodeError):
|
||||||
|
pass
|
||||||
49
tests/api/auth/test_login.py
Normal file
49
tests/api/auth/test_login.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import json
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from decnet.web.api import app
|
||||||
|
from hypothesis import given, strategies as st, settings
|
||||||
|
import httpx
|
||||||
|
from decnet.env import DECNET_ADMIN_USER, DECNET_ADMIN_PASSWORD
|
||||||
|
from ..conftest import _FUZZ_SETTINGS
|
||||||
|
|
||||||
|
def test_login_success() -> None:
|
||||||
|
with TestClient(app) as client:
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1/auth/login",
|
||||||
|
json={"username": DECNET_ADMIN_USER, "password": DECNET_ADMIN_PASSWORD}
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "access_token" in data
|
||||||
|
assert data["token_type"] == "bearer"
|
||||||
|
assert "must_change_password" in data
|
||||||
|
assert data["must_change_password"] is True
|
||||||
|
|
||||||
|
def test_login_failure() -> None:
|
||||||
|
with TestClient(app) as client:
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1/auth/login",
|
||||||
|
json={"username": DECNET_ADMIN_USER, "password": "wrongpassword"}
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1/auth/login",
|
||||||
|
json={"username": "nonexistent", "password": "wrongpassword"}
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
@settings(**_FUZZ_SETTINGS)
|
||||||
|
@given(
|
||||||
|
username=st.text(min_size=0, max_size=2048),
|
||||||
|
password=st.text(min_size=0, max_size=2048)
|
||||||
|
)
|
||||||
|
def test_fuzz_login(username: str, password: str) -> None:
|
||||||
|
"""Fuzz the login endpoint with random strings (including non-ASCII)."""
|
||||||
|
with TestClient(app) as _client:
|
||||||
|
_payload: dict[str, str] = {"username": username, "password": password}
|
||||||
|
try:
|
||||||
|
_response: httpx.Response = _client.post("/api/v1/auth/login", json=_payload)
|
||||||
|
assert _response.status_code in (200, 401, 422)
|
||||||
|
except (UnicodeEncodeError, json.JSONDecodeError):
|
||||||
|
pass
|
||||||
0
tests/api/bounty/__init__.py
Normal file
0
tests/api/bounty/__init__.py
Normal file
19
tests/api/bounty/test_get_bounties.py
Normal file
19
tests/api/bounty/test_get_bounties.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from decnet.web.api import app
|
||||||
|
|
||||||
|
def test_add_and_get_bounty(auth_token):
|
||||||
|
with TestClient(app) as client:
|
||||||
|
# We can't directly call add_bounty from API yet (it's internal to ingester)
|
||||||
|
# But we can test the endpoint returns 200 even if empty.
|
||||||
|
resp = client.get("/api/v1/bounty", headers={"Authorization": f"Bearer {auth_token}"})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert "total" in data
|
||||||
|
assert "data" in data
|
||||||
|
assert isinstance(data["data"], list)
|
||||||
|
|
||||||
|
def test_bounty_pagination(auth_token):
|
||||||
|
with TestClient(app) as client:
|
||||||
|
resp = client.get("/api/v1/bounty?limit=1&offset=0", headers={"Authorization": f"Bearer {auth_token}"})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["limit"] == 1
|
||||||
99
tests/api/conftest.py
Normal file
99
tests/api/conftest.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import pytest
|
||||||
|
from typing import Generator, Any
|
||||||
|
from pathlib import Path
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from hypothesis import HealthCheck
|
||||||
|
|
||||||
|
from decnet.web.api import app
|
||||||
|
from decnet.web.dependencies import repo
|
||||||
|
from decnet.env import DECNET_ADMIN_USER, DECNET_ADMIN_PASSWORD
|
||||||
|
import decnet.config
|
||||||
|
|
||||||
|
TEST_STATE_FILE = Path("test-decnet-state.json")
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function", autouse=True)
|
||||||
|
def setup_db() -> Generator[None, None, None]:
|
||||||
|
# Use a unique DB for each test process/thread if possible, but for now just one
|
||||||
|
repo.db_path = "test_api_decnet.db"
|
||||||
|
if os.path.exists(repo.db_path):
|
||||||
|
try:
|
||||||
|
os.remove(repo.db_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
repo.reinitialize()
|
||||||
|
yield
|
||||||
|
if os.path.exists(repo.db_path):
|
||||||
|
try:
|
||||||
|
os.remove(repo.db_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def auth_token() -> str:
|
||||||
|
with TestClient(app) as client:
|
||||||
|
resp = client.post("/api/v1/auth/login", json={"username": DECNET_ADMIN_USER, "password": DECNET_ADMIN_PASSWORD})
|
||||||
|
return resp.json()["access_token"]
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def patch_state_file(monkeypatch):
|
||||||
|
monkeypatch.setattr(decnet.config, "STATE_FILE", TEST_STATE_FILE)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_state_file():
|
||||||
|
_test_state = {
|
||||||
|
"config": {
|
||||||
|
"mode": "unihost",
|
||||||
|
"interface": "eth0",
|
||||||
|
"subnet": "192.168.1.0/24",
|
||||||
|
"gateway": "192.168.1.1",
|
||||||
|
"deckies": [
|
||||||
|
{
|
||||||
|
"name": "test-decky-1",
|
||||||
|
"ip": "192.168.1.10",
|
||||||
|
"services": ["ssh"],
|
||||||
|
"distro": "debian",
|
||||||
|
"base_image": "debian",
|
||||||
|
"hostname": "test-host-1",
|
||||||
|
"service_config": {"ssh": {"banner": "SSH-2.0-OpenSSH_8.9"}},
|
||||||
|
"archetype": "deaddeck",
|
||||||
|
"nmap_os": "linux",
|
||||||
|
"build_base": "debian:bookworm-slim",
|
||||||
|
"mutate_interval": 30,
|
||||||
|
"last_mutated": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "test-decky-2",
|
||||||
|
"ip": "192.168.1.11",
|
||||||
|
"services": ["http"],
|
||||||
|
"distro": "ubuntu",
|
||||||
|
"base_image": "ubuntu",
|
||||||
|
"hostname": "test-host-2",
|
||||||
|
"service_config": {},
|
||||||
|
"archetype": None,
|
||||||
|
"nmap_os": "linux",
|
||||||
|
"build_base": "debian:bookworm-slim",
|
||||||
|
"mutate_interval": 30,
|
||||||
|
"last_mutated": 0.0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"log_target": None,
|
||||||
|
"log_file": "test.log",
|
||||||
|
"ipvlan": False,
|
||||||
|
"mutate_interval": 30
|
||||||
|
},
|
||||||
|
"compose_path": "test-compose.yml"
|
||||||
|
}
|
||||||
|
TEST_STATE_FILE.write_text(json.dumps(_test_state))
|
||||||
|
yield _test_state
|
||||||
|
if TEST_STATE_FILE.exists():
|
||||||
|
TEST_STATE_FILE.unlink()
|
||||||
|
|
||||||
|
# Share fuzz settings across API tests
|
||||||
|
_FUZZ_SETTINGS: dict[str, Any] = {
|
||||||
|
"max_examples": 50,
|
||||||
|
"deadline": None,
|
||||||
|
"suppress_health_check": [HealthCheck.function_scoped_fixture]
|
||||||
|
}
|
||||||
0
tests/api/fleet/__init__.py
Normal file
0
tests/api/fleet/__init__.py
Normal file
16
tests/api/fleet/test_get_deckies.py
Normal file
16
tests/api/fleet/test_get_deckies.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from decnet.web.api import app
|
||||||
|
from decnet.env import DECNET_ADMIN_USER, DECNET_ADMIN_PASSWORD
|
||||||
|
|
||||||
|
def test_get_deckies_endpoint(mock_state_file):
|
||||||
|
with TestClient(app) as _client:
|
||||||
|
# Login to get token
|
||||||
|
_login_resp = _client.post("/api/v1/auth/login", json={"username": DECNET_ADMIN_USER, "password": DECNET_ADMIN_PASSWORD})
|
||||||
|
_token = _login_resp.json()["access_token"]
|
||||||
|
|
||||||
|
_response = _client.get("/api/v1/deckies", headers={"Authorization": f"Bearer {_token}"})
|
||||||
|
assert _response.status_code == 200
|
||||||
|
_data = _response.json()
|
||||||
|
assert len(_data) == 2
|
||||||
|
assert _data[0]["name"] == "test-decky-1"
|
||||||
|
assert _data[0]["service_config"]["ssh"]["banner"] == "SSH-2.0-OpenSSH_8.9"
|
||||||
0
tests/api/logs/__init__.py
Normal file
0
tests/api/logs/__init__.py
Normal file
53
tests/api/logs/test_get_logs.py
Normal file
53
tests/api/logs/test_get_logs.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
from typing import Any, Optional
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from decnet.web.api import app
|
||||||
|
from hypothesis import given, strategies as st, settings
|
||||||
|
import httpx
|
||||||
|
from decnet.env import DECNET_ADMIN_USER, DECNET_ADMIN_PASSWORD
|
||||||
|
from ..conftest import _FUZZ_SETTINGS
|
||||||
|
|
||||||
|
def test_get_logs_unauthorized() -> None:
|
||||||
|
with TestClient(app) as client:
|
||||||
|
response = client.get("/api/v1/logs")
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
def test_get_logs_success() -> None:
|
||||||
|
with TestClient(app) as client:
|
||||||
|
login_response = client.post(
|
||||||
|
"/api/v1/auth/login",
|
||||||
|
json={"username": DECNET_ADMIN_USER, "password": DECNET_ADMIN_PASSWORD}
|
||||||
|
)
|
||||||
|
token = login_response.json()["access_token"]
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1/logs",
|
||||||
|
headers={"Authorization": f"Bearer {token}"}
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "data" in data
|
||||||
|
assert data["total"] >= 0
|
||||||
|
assert isinstance(data["data"], list)
|
||||||
|
|
||||||
|
@settings(**_FUZZ_SETTINGS)
|
||||||
|
@given(
|
||||||
|
limit=st.integers(min_value=-2000, max_value=5000),
|
||||||
|
offset=st.integers(min_value=-2000, max_value=5000),
|
||||||
|
search=st.one_of(st.none(), st.text(max_size=2048))
|
||||||
|
)
|
||||||
|
def test_fuzz_get_logs(limit: int, offset: int, search: Optional[str]) -> None:
|
||||||
|
with TestClient(app) as _client:
|
||||||
|
_login_resp: httpx.Response = _client.post("/api/v1/auth/login", json={"username": DECNET_ADMIN_USER, "password": DECNET_ADMIN_PASSWORD})
|
||||||
|
_token: str = _login_resp.json()["access_token"]
|
||||||
|
|
||||||
|
_params: dict[str, Any] = {"limit": limit, "offset": offset}
|
||||||
|
if search is not None:
|
||||||
|
_params["search"] = search
|
||||||
|
|
||||||
|
_response: httpx.Response = _client.get(
|
||||||
|
"/api/v1/logs",
|
||||||
|
params=_params,
|
||||||
|
headers={"Authorization": f"Bearer {_token}"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert _response.status_code in (200, 422)
|
||||||
0
tests/api/stats/__init__.py
Normal file
0
tests/api/stats/__init__.py
Normal file
58
tests/api/stats/test_get_stats.py
Normal file
58
tests/api/stats/test_get_stats.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
from typing import Any
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from decnet.web.api import app
|
||||||
|
from hypothesis import given, strategies as st, settings
|
||||||
|
import httpx
|
||||||
|
from decnet.env import DECNET_ADMIN_USER, DECNET_ADMIN_PASSWORD
|
||||||
|
from ..conftest import _FUZZ_SETTINGS
|
||||||
|
|
||||||
|
def test_get_stats_unauthorized() -> None:
|
||||||
|
with TestClient(app) as client:
|
||||||
|
response = client.get("/api/v1/stats")
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
def test_get_stats_success() -> None:
|
||||||
|
with TestClient(app) as client:
|
||||||
|
login_response = client.post(
|
||||||
|
"/api/v1/auth/login",
|
||||||
|
json={"username": DECNET_ADMIN_USER, "password": DECNET_ADMIN_PASSWORD}
|
||||||
|
)
|
||||||
|
token = login_response.json()["access_token"]
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1/stats",
|
||||||
|
headers={"Authorization": f"Bearer {token}"}
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "total_logs" in data
|
||||||
|
assert "unique_attackers" in data
|
||||||
|
assert "active_deckies" in data
|
||||||
|
|
||||||
|
def test_stats_includes_deployed_count(mock_state_file):
|
||||||
|
with TestClient(app) as _client:
|
||||||
|
_login_resp = _client.post("/api/v1/auth/login", json={"username": DECNET_ADMIN_USER, "password": DECNET_ADMIN_PASSWORD})
|
||||||
|
_token = _login_resp.json()["access_token"]
|
||||||
|
|
||||||
|
_response = _client.get("/api/v1/stats", headers={"Authorization": f"Bearer {_token}"})
|
||||||
|
assert _response.status_code == 200
|
||||||
|
_data = _response.json()
|
||||||
|
assert "deployed_deckies" in _data
|
||||||
|
assert _data["deployed_deckies"] == 2
|
||||||
|
|
||||||
|
@settings(**_FUZZ_SETTINGS)
|
||||||
|
@given(
|
||||||
|
token=st.text(min_size=0, max_size=4096)
|
||||||
|
)
|
||||||
|
def test_fuzz_auth_header(token: str) -> None:
|
||||||
|
"""Fuzz the Authorization header with full unicode noise."""
|
||||||
|
with TestClient(app) as _client:
|
||||||
|
try:
|
||||||
|
_response: httpx.Response = _client.get(
|
||||||
|
"/api/v1/stats",
|
||||||
|
headers={"Authorization": f"Bearer {token}"}
|
||||||
|
)
|
||||||
|
assert _response.status_code in (401, 422)
|
||||||
|
except (UnicodeEncodeError, httpx.InvalidURL, httpx.CookieConflict):
|
||||||
|
# Expected client-side rejection of invalid header characters
|
||||||
|
pass
|
||||||
Reference in New Issue
Block a user