more test improvements & new tests
Continuous Integration / backend-tests (push) Successful in 36s
Continuous Integration / frontend-check (push) Successful in 18s
Continuous Integration / e2e-tests (push) Successful in 5m13s

This commit is contained in:
2026-05-05 17:26:03 -04:00
parent f44895d40f
commit ae74a0bf02
5 changed files with 399 additions and 3 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ def get_exclusion_spec(db_session: Session) -> Optional[pathspec.PathSpec]:
for pattern in settings_record.value.splitlines()
if pattern.strip()
]
return pathspec.PathSpec.from_lines("gitwildmatch", exclusion_patterns)
return pathspec.PathSpec.from_lines("gitignore", exclusion_patterns)
def get_ignored_status(
+2 -2
View File
@@ -127,7 +127,7 @@ def test_exclusions(
total_files=0, total_size=0, matched_count=0, matched_size=0, sample=[]
)
spec = pathspec.PathSpec.from_lines("gitwildmatch", patterns)
spec = pathspec.PathSpec.from_lines("gitignore", patterns)
all_files = (
db_session.query(models.FilesystemState)
@@ -179,7 +179,7 @@ def download_exclusion_report(
if not patterns:
raise HTTPException(status_code=400, detail="No patterns provided")
spec = pathspec.PathSpec.from_lines("gitwildmatch", patterns)
spec = pathspec.PathSpec.from_lines("gitignore", patterns)
all_files = (
db_session.query(models.FilesystemState)
+196
View File
@@ -0,0 +1,196 @@
from app.db import models
# ── Settings CRUD ──
def test_get_settings_empty(client):
"""Tests retrieving settings when none are set."""
response = client.get("/system/settings")
assert response.status_code == 200
assert response.json() == {}
def test_update_settings(client):
"""Tests updating a system setting."""
response = client.post(
"/system/settings", json={"key": "schedule_scan", "value": "0 2 * * *"}
)
assert response.status_code == 200
assert response.json() == {"message": "Setting committed."}
# Verify retrieval
response = client.get("/system/settings")
assert response.json()["schedule_scan"] == "0 2 * * *"
def test_update_settings_triggers_scheduler_reload(client, mocker):
"""Tests that updating schedule_scan reloads the scheduler."""
from app.services.scheduler import scheduler_manager
reload_spy = mocker.spy(scheduler_manager, "reload")
response = client.post(
"/system/settings", json={"key": "schedule_scan", "value": "0 3 * * *"}
)
assert response.status_code == 200
reload_spy.assert_called_once()
def test_update_global_exclusions_recomputes_policy(client, db_session, mocker):
"""Tests that updating global_exclusions triggers policy recompute."""
recompute_spy = mocker.patch("app.api.system.settings.recompute_exclusion_policy")
response = client.post(
"/system/settings",
json={"key": "global_exclusions", "value": "*.tmp\n*.log"},
)
assert response.status_code == 200
recompute_spy.assert_called_once()
# ── Exclusion Testing ──
def test_test_exclusions_empty_patterns(client):
"""Tests exclusion test with empty patterns returns zeros."""
response = client.post(
"/system/settings/test-exclusions",
json={"patterns": "", "limit": 10},
)
assert response.status_code == 200
data = response.json()
assert data["total_files"] == 0
assert data["matched_count"] == 0
assert data["sample"] == []
def test_test_exclusions_matches_files(client, db_session):
"""Tests exclusion patterns against indexed files."""
db_session.add_all(
[
models.FilesystemState(
file_path="/data/file.txt", size=100, mtime=1000, is_deleted=False
),
models.FilesystemState(
file_path="/data/temp.tmp", size=50, mtime=1000, is_deleted=False
),
models.FilesystemState(
file_path="/data/debug.log", size=200, mtime=1000, is_deleted=False
),
]
)
db_session.commit()
response = client.post(
"/system/settings/test-exclusions",
json={"patterns": "*.tmp\n*.log", "limit": 10},
)
assert response.status_code == 200
data = response.json()
assert data["total_files"] == 3
assert data["matched_count"] == 2
assert data["matched_size"] == 250
assert len(data["sample"]) == 2
def test_test_exclusions_deleted_files_excluded(client, db_session):
"""Tests that deleted files are excluded from exclusion testing."""
db_session.add_all(
[
models.FilesystemState(
file_path="/data/keep.txt",
size=100,
mtime=1000,
is_deleted=False,
),
models.FilesystemState(
file_path="/data/old.tmp",
size=50,
mtime=1000,
is_deleted=True,
),
]
)
db_session.commit()
response = client.post(
"/system/settings/test-exclusions",
json={"patterns": "*.tmp", "limit": 10},
)
assert response.status_code == 200
data = response.json()
assert data["total_files"] == 1
assert data["matched_count"] == 0
# ── Exclusion Report Download ──
def test_download_exclusion_report(client, db_session):
"""Tests CSV report generation for exclusion matches."""
db_session.add(
models.FilesystemState(
file_path="/data/target.log", size=100, mtime=1000, is_deleted=False
)
)
db_session.commit()
response = client.post(
"/system/settings/test-exclusions/download",
json={"patterns": "*.log", "limit": 10},
)
assert response.status_code == 200
assert response.headers["content-type"] == "text/csv; charset=utf-8"
assert "exclusion_report.csv" in response.headers["content-disposition"]
content = response.content.decode("utf-8")
assert "path,size,mtime,sha256_hash" in content
assert "target.log" in content
def test_download_exclusion_report_no_patterns(client):
"""Tests download with empty patterns returns 400."""
response = client.post(
"/system/settings/test-exclusions/download",
json={"patterns": "", "limit": 10},
)
assert response.status_code == 400
assert "No patterns provided" in response.json()["detail"]
# ── Secrets Keystore (complementing test_api_system.py) ──
def test_create_secret(client):
"""Tests creating a secret."""
response = client.post(
"/system/secrets", json={"name": "api-key", "value": "secret123"}
)
assert response.status_code == 200
assert "stored" in response.json()["message"]
response = client.get("/system/secrets")
assert "api-key" in response.json()
def test_get_secret_value(client):
"""Tests retrieving a secret value."""
client.post("/system/secrets", json={"name": "key-1", "value": "val-1"})
response = client.get("/system/secrets/key-1")
assert response.status_code == 200
assert response.json()["value"] == "val-1"
def test_delete_secret(client):
"""Tests deleting a secret."""
client.post("/system/secrets", json={"name": "to-delete", "value": "x"})
response = client.request("DELETE", "/system/secrets", json={"name": "to-delete"})
assert response.status_code == 200
response = client.get("/system/secrets")
assert "to-delete" not in response.json()
def test_delete_secret_not_found(client):
"""Tests deleting a non-existent secret returns 404."""
response = client.request("DELETE", "/system/secrets", json={"name": "missing"})
assert response.status_code == 404
+65
View File
@@ -1,3 +1,4 @@
import json
from datetime import datetime, timezone
from app.db import models
@@ -560,3 +561,67 @@ def test_test_notification_invalid_url(client):
)
assert response.status_code == 500
assert "Failed to dispatch test alert" in response.json()["detail"]
# ── Host Directory Listing ──
def test_ls_traversal_rejected(client):
"""Tests that path traversal attempts are blocked."""
response = client.get("/system/ls?path=/etc/../secret")
assert response.status_code == 403
assert "Path traversal not allowed" in response.json()["detail"]
def test_ls_nonexistent_path(client):
"""Tests listing a non-existent directory returns empty list."""
response = client.get("/system/ls?path=/nonexistent_path_12345")
assert response.status_code == 200
assert response.json() == []
# ── System Tree ──
def test_system_tree_root(client, db_session):
"""Tests system tree at ROOT returns configured source roots."""
db_session.add(models.SystemSetting(key="source_roots", value='["/source_data"]'))
db_session.commit()
response = client.get("/system/tree")
assert response.status_code == 200
data = response.json()
assert len(data) == 1
assert data[0]["name"] == "/source_data"
assert data[0]["has_children"] is True
def test_system_tree_subdirectory(client, db_session):
"""Tests system tree browsing a subdirectory."""
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
db_session.add(
models.SystemSetting(key="source_roots", value=json.dumps([tmpdir]))
)
db_session.commit()
# Create a subdirectory
import os
os.makedirs(os.path.join(tmpdir, "subdir"))
response = client.get(f"/system/tree?path={tmpdir}")
assert response.status_code == 200
data = response.json()
assert len(data) == 1
assert data[0]["name"] == "subdir"
def test_system_tree_outside_roots(client, db_session):
"""Tests tree browsing outside roots returns 403."""
db_session.add(models.SystemSetting(key="source_roots", value='["/source_data"]'))
db_session.commit()
response = client.get("/system/tree?path=/etc")
assert response.status_code == 403
+135
View File
@@ -0,0 +1,135 @@
from app.services.scheduler import SchedulerService
from app.db import models
def test_scheduler_start_stop():
"""Tests scheduler lifecycle (start, stop, idempotent)."""
scheduler = SchedulerService()
assert not scheduler.scheduler.running
scheduler.start()
assert scheduler.scheduler.running
# Idempotent start
scheduler.start()
assert scheduler.scheduler.running
scheduler.stop()
assert not scheduler.scheduler.running
# Idempotent stop
scheduler.stop()
assert not scheduler.scheduler.running
def test_scheduler_load_schedules_empty():
"""Tests load_schedules with no cron settings configured."""
scheduler = SchedulerService()
scheduler.start()
scheduler.load_schedules()
# No jobs should be registered
assert scheduler.scheduler.get_job("system_scan") is None
assert scheduler.scheduler.get_job("system_archival") is None
scheduler.stop()
def test_scheduler_load_schedules_with_scan(db_session):
"""Tests load_schedules picks up a scan schedule from settings."""
db_session.add(models.SystemSetting(key="schedule_scan", value="0 2 * * *"))
db_session.commit()
scheduler = SchedulerService()
scheduler.start()
scheduler.load_schedules()
job = scheduler.scheduler.get_job("system_scan")
assert job is not None
assert job.id == "system_scan"
scheduler.stop()
def test_scheduler_add_remove_job():
"""Tests adding and removing scheduled jobs."""
scheduler = SchedulerService()
scheduler.start()
def dummy_job():
pass
scheduler.add_job("test_job", dummy_job, "0 0 * * *")
assert scheduler.scheduler.get_job("test_job") is not None
scheduler.remove_job("test_job")
assert scheduler.scheduler.get_job("test_job") is None
# Idempotent remove
scheduler.remove_job("test_job")
assert scheduler.scheduler.get_job("test_job") is None
scheduler.stop()
def test_scheduler_add_job_empty_cron():
"""Tests that empty/whitespace cron expression removes the job."""
scheduler = SchedulerService()
scheduler.start()
def dummy_job():
pass
scheduler.add_job("test_job", dummy_job, "0 0 * * *")
assert scheduler.scheduler.get_job("test_job") is not None
# Empty string should remove
scheduler.add_job("test_job", dummy_job, " ")
assert scheduler.scheduler.get_job("test_job") is None
scheduler.stop()
def test_scheduler_reload(db_session, mocker):
"""Tests reload calls load_schedules."""
db_session.add(models.SystemSetting(key="schedule_scan", value="0 3 * * *"))
db_session.commit()
scheduler = SchedulerService()
scheduler.start()
load_spy = mocker.spy(scheduler, "load_schedules")
scheduler.reload()
load_spy.assert_called_once()
job = scheduler.scheduler.get_job("system_scan")
assert job is not None
scheduler.stop()
def test_scheduler_run_system_scan_skips_when_running(mocker):
"""Tests run_system_scan is skipped when scanner_manager is already running."""
scheduler = SchedulerService()
mocker.patch("app.services.scheduler.scanner_manager.is_running", True)
scan_sources_spy = mocker.patch(
"app.services.scheduler.scanner_manager.scan_sources"
)
scheduler.run_system_scan()
scan_sources_spy.assert_not_called()
def test_scheduler_run_system_archival_no_online_media(db_session, mocker):
"""Tests run_system_archival skips when no active media is online."""
scheduler = SchedulerService()
# No media in DB
run_backup_spy = mocker.patch("app.services.scheduler.archiver_manager.run_backup")
scheduler.run_system_archival()
run_backup_spy.assert_not_called()