more test improvements & new tests
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user