more test coverage
This commit is contained in:
@@ -58,3 +58,54 @@ def test_backup_history_populated(client, db_session):
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) == 1
|
||||
assert response.json()[0]["status"] == "COMPLETED"
|
||||
|
||||
|
||||
def test_trigger_backup_already_running(client, db_session):
|
||||
"""Tests triggering backup when one is already active returns 400."""
|
||||
media = models.StorageMedia(
|
||||
media_type="hdd", identifier="DISK_1", capacity=1000, status="active"
|
||||
)
|
||||
db_session.add(media)
|
||||
db_session.flush()
|
||||
|
||||
# Create an active backup job
|
||||
active_job = models.Job(job_type="BACKUP", status="RUNNING", is_cancelled=False)
|
||||
db_session.add(active_job)
|
||||
db_session.commit()
|
||||
|
||||
response = client.post(f"/backups/trigger/{media.id}")
|
||||
assert response.status_code == 400
|
||||
assert "already running" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_trigger_auto_backup_no_media(client):
|
||||
"""Tests auto backup with no active media returns 400."""
|
||||
response = client.post("/backups/trigger/auto")
|
||||
assert response.status_code == 400
|
||||
assert "No active media" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_trigger_auto_backup_already_running(client, db_session):
|
||||
"""Tests auto backup when one is already active returns 400."""
|
||||
active_job = models.Job(job_type="BACKUP", status="PENDING", is_cancelled=False)
|
||||
db_session.add(active_job)
|
||||
db_session.commit()
|
||||
|
||||
response = client.post("/backups/trigger/auto")
|
||||
assert response.status_code == 400
|
||||
assert "already running" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_trigger_auto_backup_success(client, db_session):
|
||||
"""Tests triggering auto backup creates a job."""
|
||||
media = models.StorageMedia(
|
||||
media_type="hdd", identifier="AUTO_DISK", capacity=1000000000, status="active"
|
||||
)
|
||||
db_session.add(media)
|
||||
db_session.commit()
|
||||
|
||||
response = client.post("/backups/trigger/auto")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "job_id" in data
|
||||
assert "Auto-archival job submitted" in data["message"]
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from app.db import models
|
||||
|
||||
|
||||
# ── list_discrepancies ──
|
||||
|
||||
|
||||
def test_list_discrepancies_empty(client):
|
||||
"""Tests listing discrepancies when none exist."""
|
||||
response = client.get("/system/discrepancies")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
|
||||
def test_list_discrepancies_deleted_file(client, db_session):
|
||||
"""Tests listing a confirmed-deleted file in discrepancies."""
|
||||
file_record = models.FilesystemState(
|
||||
file_path="/data/old.txt",
|
||||
size=100,
|
||||
mtime=1000,
|
||||
is_deleted=True,
|
||||
is_ignored=False,
|
||||
sha256_hash=None,
|
||||
)
|
||||
db_session.add(file_record)
|
||||
db_session.commit()
|
||||
|
||||
response = client.get("/system/discrepancies")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 1
|
||||
assert data[0]["path"] == "/data/old.txt"
|
||||
assert data[0]["is_deleted"] is True
|
||||
|
||||
|
||||
def test_list_discrepancies_unhashed_missing(client, db_session, tmp_path):
|
||||
"""Tests listing an unhashed file that is missing from disk."""
|
||||
# File path that does not exist on disk
|
||||
missing_path = str(tmp_path / "nonexistent" / "file.txt")
|
||||
file_record = models.FilesystemState(
|
||||
file_path=missing_path,
|
||||
size=100,
|
||||
mtime=1000,
|
||||
sha256_hash=None,
|
||||
is_deleted=False,
|
||||
is_ignored=False,
|
||||
)
|
||||
db_session.add(file_record)
|
||||
db_session.commit()
|
||||
|
||||
response = client.get("/system/discrepancies")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 1
|
||||
assert data[0]["path"] == missing_path
|
||||
assert data[0]["is_deleted"] is False
|
||||
|
||||
|
||||
def test_list_discrepancies_ignored_excluded(client, db_session):
|
||||
"""Tests that ignored files are excluded from discrepancies."""
|
||||
file_record = models.FilesystemState(
|
||||
file_path="/data/ignored.txt",
|
||||
size=100,
|
||||
mtime=1000,
|
||||
is_deleted=True,
|
||||
is_ignored=True,
|
||||
)
|
||||
db_session.add(file_record)
|
||||
db_session.commit()
|
||||
|
||||
response = client.get("/system/discrepancies")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
|
||||
def test_list_discrepancies_acknowledged_excluded(client, db_session):
|
||||
"""Tests that acknowledged discrepancies are excluded."""
|
||||
file_record = models.FilesystemState(
|
||||
file_path="/data/dismissed.txt",
|
||||
size=100,
|
||||
mtime=1000,
|
||||
is_deleted=True,
|
||||
is_ignored=False,
|
||||
missing_acknowledged_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db_session.add(file_record)
|
||||
db_session.commit()
|
||||
|
||||
response = client.get("/system/discrepancies")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
|
||||
def test_list_discrepancies_has_versions_flag(client, db_session):
|
||||
"""Tests that has_versions is set based on active/full media."""
|
||||
active_media = models.StorageMedia(
|
||||
media_type="hdd", identifier="M1", capacity=1000, status="active"
|
||||
)
|
||||
db_session.add(active_media)
|
||||
db_session.flush()
|
||||
|
||||
file1 = models.FilesystemState(
|
||||
file_path="/data/deleted_with_backup.txt",
|
||||
size=100,
|
||||
mtime=1000,
|
||||
is_deleted=True,
|
||||
is_ignored=False,
|
||||
)
|
||||
db_session.add(file1)
|
||||
db_session.flush()
|
||||
|
||||
db_session.add(
|
||||
models.FileVersion(
|
||||
filesystem_state_id=file1.id,
|
||||
media_id=active_media.id,
|
||||
file_number="1",
|
||||
offset_start=0,
|
||||
offset_end=100,
|
||||
)
|
||||
)
|
||||
db_session.commit()
|
||||
|
||||
response = client.get("/system/discrepancies")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 1
|
||||
assert data[0]["has_versions"] is True
|
||||
|
||||
|
||||
# ── confirm_discrepancy ──
|
||||
|
||||
|
||||
def test_confirm_discrepancy(client, db_session):
|
||||
"""Tests confirming a file as deleted."""
|
||||
file_record = models.FilesystemState(
|
||||
file_path="/data/verify.txt", size=50, mtime=2000, is_deleted=False
|
||||
)
|
||||
db_session.add(file_record)
|
||||
db_session.commit()
|
||||
|
||||
response = client.post(f"/system/discrepancies/{file_record.id}/confirm")
|
||||
assert response.status_code == 200
|
||||
assert "marked as deleted" in response.json()["message"]
|
||||
|
||||
db_session.expire_all()
|
||||
db_session.refresh(file_record)
|
||||
assert file_record.is_deleted is True
|
||||
|
||||
|
||||
def test_confirm_discrepancy_not_found(client):
|
||||
"""Tests confirming a non-existent file returns 404."""
|
||||
response = client.post("/system/discrepancies/9999/confirm")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
# ── dismiss_discrepancy ──
|
||||
|
||||
|
||||
def test_dismiss_discrepancy(client, db_session):
|
||||
"""Tests dismissing a deleted file."""
|
||||
file_record = models.FilesystemState(
|
||||
file_path="/data/dismiss.txt", size=50, mtime=2000, is_deleted=True
|
||||
)
|
||||
db_session.add(file_record)
|
||||
db_session.commit()
|
||||
|
||||
response = client.post(f"/system/discrepancies/{file_record.id}/dismiss")
|
||||
assert response.status_code == 200
|
||||
assert "dismissed" in response.json()["message"]
|
||||
|
||||
db_session.expire_all()
|
||||
db_session.refresh(file_record)
|
||||
assert file_record.missing_acknowledged_at is not None
|
||||
|
||||
|
||||
def test_undo_dismiss_discrepancy(client, db_session):
|
||||
"""Tests undoing a dismissed discrepancy."""
|
||||
file_record = models.FilesystemState(
|
||||
file_path="/data/undo.txt",
|
||||
size=50,
|
||||
mtime=2000,
|
||||
is_deleted=True,
|
||||
missing_acknowledged_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db_session.add(file_record)
|
||||
db_session.commit()
|
||||
|
||||
response = client.post(f"/system/discrepancies/{file_record.id}/undo-dismiss")
|
||||
assert response.status_code == 200
|
||||
assert "undo" in response.json()["message"].lower()
|
||||
|
||||
db_session.expire_all()
|
||||
db_session.refresh(file_record)
|
||||
assert file_record.missing_acknowledged_at is None
|
||||
|
||||
|
||||
# ── delete_discrepancy ──
|
||||
|
||||
|
||||
def test_delete_discrepancy_hard_delete(client, db_session):
|
||||
"""Tests hard-deleting a file record and its versions/cart entries."""
|
||||
media = models.StorageMedia(
|
||||
media_type="hdd", identifier="M1", capacity=1000, status="active"
|
||||
)
|
||||
db_session.add(media)
|
||||
db_session.flush()
|
||||
|
||||
file_record = models.FilesystemState(
|
||||
file_path="/data/hard_delete.txt", size=100, mtime=1000, is_deleted=True
|
||||
)
|
||||
db_session.add(file_record)
|
||||
db_session.flush()
|
||||
|
||||
db_session.add(
|
||||
models.FileVersion(
|
||||
filesystem_state_id=file_record.id,
|
||||
media_id=media.id,
|
||||
file_number="1",
|
||||
offset_start=0,
|
||||
offset_end=100,
|
||||
)
|
||||
)
|
||||
db_session.add(models.RestoreCart(filesystem_state_id=file_record.id))
|
||||
db_session.commit()
|
||||
|
||||
file_id = file_record.id
|
||||
|
||||
response = client.delete(f"/system/discrepancies/{file_id}")
|
||||
assert response.status_code == 200
|
||||
|
||||
db_session.expire_all()
|
||||
assert db_session.get(models.FilesystemState, file_id) is None
|
||||
assert (
|
||||
db_session.query(models.FileVersion)
|
||||
.filter_by(filesystem_state_id=file_id)
|
||||
.first()
|
||||
is None
|
||||
)
|
||||
assert (
|
||||
db_session.query(models.RestoreCart)
|
||||
.filter_by(filesystem_state_id=file_id)
|
||||
.first()
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_delete_discrepancy_not_found(client):
|
||||
"""Tests deleting a non-existent discrepancy returns 404."""
|
||||
response = client.delete("/system/discrepancies/9999")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
# ── batch operations ──
|
||||
|
||||
|
||||
def test_batch_confirm_discrepancies(client, db_session):
|
||||
"""Tests batch confirming files as deleted."""
|
||||
file1 = models.FilesystemState(
|
||||
file_path="/data/batch1.txt", size=100, mtime=1000, is_deleted=False
|
||||
)
|
||||
file2 = models.FilesystemState(
|
||||
file_path="/data/batch2.txt", size=200, mtime=1000, is_deleted=False
|
||||
)
|
||||
db_session.add_all([file1, file2])
|
||||
db_session.commit()
|
||||
|
||||
response = client.post(
|
||||
"/system/discrepancies/batch/confirm",
|
||||
json={"ids": [file1.id, file2.id]},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["count"] == 2
|
||||
|
||||
db_session.expire_all()
|
||||
assert db_session.get(models.FilesystemState, file1.id).is_deleted is True
|
||||
assert db_session.get(models.FilesystemState, file2.id).is_deleted is True
|
||||
|
||||
|
||||
def test_batch_dismiss_discrepancies(client, db_session):
|
||||
"""Tests batch dismissing discrepancies."""
|
||||
file1 = models.FilesystemState(
|
||||
file_path="/data/dismiss1.txt", size=100, mtime=1000, is_deleted=True
|
||||
)
|
||||
db_session.add(file1)
|
||||
db_session.commit()
|
||||
|
||||
response = client.post(
|
||||
"/system/discrepancies/batch/dismiss",
|
||||
json={"ids": [file1.id]},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["count"] == 1
|
||||
|
||||
db_session.expire_all()
|
||||
assert (
|
||||
db_session.get(models.FilesystemState, file1.id).missing_acknowledged_at
|
||||
is not None
|
||||
)
|
||||
|
||||
|
||||
def test_batch_delete_discrepancies(client, db_session):
|
||||
"""Tests batch hard-deleting discrepancy records."""
|
||||
file1 = models.FilesystemState(
|
||||
file_path="/data/del1.txt", size=100, mtime=1000, is_deleted=True
|
||||
)
|
||||
db_session.add(file1)
|
||||
db_session.commit()
|
||||
|
||||
response = client.post(
|
||||
"/system/discrepancies/batch/delete",
|
||||
json={"ids": [file1.id]},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["count"] == 1
|
||||
|
||||
db_session.expire_all()
|
||||
assert db_session.get(models.FilesystemState, file1.id) is None
|
||||
|
||||
|
||||
def test_batch_resolve_discrepancies(client, db_session):
|
||||
"""Tests smart batch resolve: recoverable -> queue, lost -> confirm delete."""
|
||||
media = models.StorageMedia(
|
||||
media_type="hdd", identifier="M1", capacity=1000, status="active"
|
||||
)
|
||||
db_session.add(media)
|
||||
db_session.flush()
|
||||
|
||||
# File with backup (recoverable)
|
||||
file_recover = models.FilesystemState(
|
||||
file_path="/data/recover.txt", size=100, mtime=1000, is_deleted=True
|
||||
)
|
||||
# File without backup (lost)
|
||||
file_lost = models.FilesystemState(
|
||||
file_path="/data/lost.txt", size=200, mtime=1000, is_deleted=True
|
||||
)
|
||||
db_session.add_all([file_recover, file_lost])
|
||||
db_session.flush()
|
||||
|
||||
db_session.add(
|
||||
models.FileVersion(
|
||||
filesystem_state_id=file_recover.id,
|
||||
media_id=media.id,
|
||||
file_number="1",
|
||||
offset_start=0,
|
||||
offset_end=100,
|
||||
)
|
||||
)
|
||||
db_session.commit()
|
||||
|
||||
response = client.post(
|
||||
"/system/discrepancies/batch/resolve",
|
||||
json={"ids": [file_recover.id, file_lost.id]},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["recovered_count"] == 1
|
||||
assert data["lost_count"] == 1
|
||||
assert "/data/recover.txt" in data["recovered_paths"]
|
||||
assert "/data/lost.txt" in data["lost_paths"]
|
||||
|
||||
db_session.expire_all()
|
||||
# Recoverable should be in restore cart
|
||||
assert (
|
||||
db_session.query(models.RestoreCart)
|
||||
.filter_by(filesystem_state_id=file_recover.id)
|
||||
.first()
|
||||
is not None
|
||||
)
|
||||
# Lost should be marked deleted and dismissed
|
||||
lost_record = db_session.get(models.FilesystemState, file_lost.id)
|
||||
assert lost_record.is_deleted is True
|
||||
assert lost_record.missing_acknowledged_at is not None
|
||||
|
||||
|
||||
def test_batch_resolve_by_path_prefix(client, db_session):
|
||||
"""Tests batch resolve using path_prefix instead of ids."""
|
||||
file1 = models.FilesystemState(
|
||||
file_path="/data/lost1.txt", size=100, mtime=1000, is_deleted=True
|
||||
)
|
||||
db_session.add(file1)
|
||||
db_session.commit()
|
||||
|
||||
response = client.post(
|
||||
"/system/discrepancies/batch/resolve",
|
||||
json={"path_prefix": "/data"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["lost_count"] == 1
|
||||
|
||||
|
||||
def test_batch_action_no_ids_or_prefix(client):
|
||||
"""Tests batch actions without ids or path_prefix return 400."""
|
||||
for endpoint in ["confirm", "dismiss", "delete", "resolve"]:
|
||||
response = client.post(f"/system/discrepancies/batch/{endpoint}", json={})
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
# ── discrepancy tree ──
|
||||
|
||||
|
||||
def test_discrepancy_tree_empty(client):
|
||||
"""Tests discrepancy tree when no discrepancies exist."""
|
||||
response = client.get("/system/discrepancies/tree")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
|
||||
def test_discrepancy_tree_root(client, db_session):
|
||||
"""Tests discrepancy tree at ROOT level."""
|
||||
file1 = models.FilesystemState(
|
||||
file_path="data/file1.txt", size=100, mtime=1000, is_deleted=True
|
||||
)
|
||||
db_session.add(file1)
|
||||
db_session.commit()
|
||||
|
||||
response = client.get("/system/discrepancies/tree")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) >= 1
|
||||
|
||||
|
||||
# ── discrepancy browse ──
|
||||
|
||||
|
||||
def test_discrepancy_browse_empty(client):
|
||||
"""Tests discrepancy browse when no discrepancies exist."""
|
||||
response = client.get("/system/discrepancies/browse")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["files"] == []
|
||||
|
||||
|
||||
def test_discrepancy_browse_with_files(client, db_session):
|
||||
"""Tests discrepancy browse returns files and directories."""
|
||||
file1 = models.FilesystemState(
|
||||
file_path="data/sub/file1.txt", size=100, mtime=1000, is_deleted=True
|
||||
)
|
||||
db_session.add(file1)
|
||||
db_session.commit()
|
||||
|
||||
response = client.get("/system/discrepancies/browse?path=ROOT")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "files" in data
|
||||
@@ -560,3 +560,137 @@ def test_update_status_to_retired_purges_versions(client, db_session):
|
||||
{"media_id": media.id},
|
||||
).scalar()
|
||||
assert result == 0
|
||||
|
||||
|
||||
# ── Archive Tree ──
|
||||
|
||||
|
||||
def test_archive_tree_with_versions(client, db_session):
|
||||
"""Tests archive tree returns source roots with versions."""
|
||||
db_session.add(models.SystemSetting(key="source_roots", value=json.dumps(["data"])))
|
||||
db_session.flush()
|
||||
|
||||
media = models.StorageMedia(
|
||||
media_type="hdd", identifier="M1", capacity=1000, status="active"
|
||||
)
|
||||
db_session.add(media)
|
||||
db_session.flush()
|
||||
|
||||
file1 = models.FilesystemState(file_path="data/file1.txt", size=100, mtime=1000)
|
||||
db_session.add(file1)
|
||||
db_session.flush()
|
||||
|
||||
db_session.add(
|
||||
models.FileVersion(
|
||||
filesystem_state_id=file1.id,
|
||||
media_id=media.id,
|
||||
file_number="1",
|
||||
offset_start=0,
|
||||
offset_end=100,
|
||||
)
|
||||
)
|
||||
db_session.commit()
|
||||
|
||||
response = client.get("/archive/tree")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 1
|
||||
assert data[0]["name"] == "data"
|
||||
assert data[0]["has_children"] is True
|
||||
|
||||
|
||||
def test_archive_tree_nested_directories(client, db_session):
|
||||
"""Tests archive tree with nested directories."""
|
||||
db_session.add(models.SystemSetting(key="source_roots", value=json.dumps(["data"])))
|
||||
db_session.flush()
|
||||
|
||||
media = models.StorageMedia(
|
||||
media_type="hdd", identifier="M1", capacity=1000, status="active"
|
||||
)
|
||||
db_session.add(media)
|
||||
db_session.flush()
|
||||
|
||||
file1 = models.FilesystemState(
|
||||
file_path="data/subdir/file1.txt", size=100, mtime=1000
|
||||
)
|
||||
db_session.add(file1)
|
||||
db_session.flush()
|
||||
|
||||
db_session.add(
|
||||
models.FileVersion(
|
||||
filesystem_state_id=file1.id,
|
||||
media_id=media.id,
|
||||
file_number="1",
|
||||
offset_start=0,
|
||||
offset_end=100,
|
||||
)
|
||||
)
|
||||
db_session.commit()
|
||||
|
||||
response = client.get("/archive/tree")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 1
|
||||
assert data[0]["name"] == "data"
|
||||
|
||||
|
||||
def test_archive_tree_empty_no_versions(client, db_session):
|
||||
"""Tests archive tree excludes roots with no versions."""
|
||||
db_session.add(models.SystemSetting(key="source_roots", value=json.dumps(["data"])))
|
||||
db_session.flush()
|
||||
|
||||
# Add file but no versions
|
||||
file1 = models.FilesystemState(file_path="data/file1.txt", size=100, mtime=1000)
|
||||
db_session.add(file1)
|
||||
db_session.commit()
|
||||
|
||||
response = client.get("/archive/tree")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
|
||||
# ── Metadata Directory ──
|
||||
|
||||
|
||||
def test_metadata_directory(client, db_session):
|
||||
"""Tests metadata endpoint for a directory returns aggregated stats."""
|
||||
db_session.add(models.SystemSetting(key="source_roots", value=json.dumps(["data"])))
|
||||
db_session.flush()
|
||||
|
||||
media = models.StorageMedia(
|
||||
media_type="hdd", identifier="M1", capacity=1000, status="active"
|
||||
)
|
||||
db_session.add(media)
|
||||
db_session.flush()
|
||||
|
||||
file1 = models.FilesystemState(file_path="data/sub/file1.txt", size=100, mtime=1000)
|
||||
file2 = models.FilesystemState(file_path="data/sub/file2.txt", size=200, mtime=1000)
|
||||
db_session.add_all([file1, file2])
|
||||
db_session.flush()
|
||||
|
||||
db_session.add(
|
||||
models.FileVersion(
|
||||
filesystem_state_id=file1.id,
|
||||
media_id=media.id,
|
||||
file_number="1",
|
||||
offset_start=0,
|
||||
offset_end=100,
|
||||
)
|
||||
)
|
||||
db_session.add(
|
||||
models.FileVersion(
|
||||
filesystem_state_id=file2.id,
|
||||
media_id=media.id,
|
||||
file_number="2",
|
||||
offset_start=0,
|
||||
offset_end=200,
|
||||
)
|
||||
)
|
||||
db_session.commit()
|
||||
|
||||
response = client.get("/archive/metadata?path=data/sub")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["type"] == "directory"
|
||||
assert data["child_count"] == 2
|
||||
assert data["size"] == 300
|
||||
|
||||
@@ -504,3 +504,177 @@ def test_update_existing_secret(client):
|
||||
response = client.get("/system/secrets/ rotating-key ")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["value"] == "new-value"
|
||||
|
||||
|
||||
# ── Filesystem Browse ──
|
||||
|
||||
|
||||
def test_filesystem_browse_root(client, db_session):
|
||||
"""Tests browsing the filesystem at ROOT level."""
|
||||
db_session.add(models.SystemSetting(key="source_roots", value='["/source_data"]'))
|
||||
db_session.commit()
|
||||
|
||||
response = client.get("/system/browse?path=ROOT")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["files"]) == 1
|
||||
assert data["files"][0]["path"] == "/source_data"
|
||||
assert data["files"][0]["type"] == "directory"
|
||||
|
||||
|
||||
def test_filesystem_browse_subdirectory(client, db_session):
|
||||
"""Tests browsing a subdirectory of the filesystem index."""
|
||||
db_session.add(models.SystemSetting(key="source_roots", value='["/source_data"]'))
|
||||
db_session.flush()
|
||||
|
||||
file1 = models.FilesystemState(
|
||||
file_path="/source_data/subdir/file1.txt", size=100, mtime=1000
|
||||
)
|
||||
db_session.add(file1)
|
||||
db_session.commit()
|
||||
|
||||
response = client.get("/system/browse?path=/source_data/subdir")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["files"]) == 1
|
||||
assert data["files"][0]["name"] == "file1.txt"
|
||||
assert data["files"][0]["type"] == "file"
|
||||
|
||||
|
||||
def test_filesystem_browse_outside_roots(client, db_session):
|
||||
"""Tests browsing outside configured roots returns 403."""
|
||||
db_session.add(models.SystemSetting(key="source_roots", value='["/source_data"]'))
|
||||
db_session.commit()
|
||||
|
||||
response = client.get("/system/browse?path=/etc")
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
# ── Filesystem Search ──
|
||||
|
||||
|
||||
def test_filesystem_search_too_short(client):
|
||||
"""Tests search with query < 3 chars returns empty list."""
|
||||
response = client.get("/system/search?q=ab")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
|
||||
# ── Hardware Discover ──
|
||||
|
||||
|
||||
def test_discover_hardware_empty(client):
|
||||
"""Tests hardware discovery endpoint returns a list (may contain real mounts)."""
|
||||
response = client.get("/system/hardware/discover")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
# Each discovered node should have required fields if present
|
||||
for node in data:
|
||||
assert "type" in node
|
||||
assert "identifier" in node
|
||||
assert "is_registered" in node
|
||||
|
||||
|
||||
# ── Hardware Ignore ──
|
||||
|
||||
|
||||
def test_ignore_hardware_duplicate(client):
|
||||
"""Tests ignoring the same hardware twice is idempotent."""
|
||||
client.post("/system/hardware/ignore", json={"identifier": "DISK_DUP"})
|
||||
response = client.post("/system/hardware/ignore", json={"identifier": "DISK_DUP"})
|
||||
assert response.status_code == 200
|
||||
|
||||
response = client.get("/system/settings")
|
||||
assert response.json()["ignored_hardware"].count("DISK_DUP") == 1
|
||||
|
||||
|
||||
# ── Database Export ──
|
||||
|
||||
|
||||
def test_database_export(client):
|
||||
"""Tests database export endpoint returns a file response."""
|
||||
response = client.get("/system/database/export")
|
||||
# May return 200 with file or 404 if db path not found
|
||||
assert response.status_code in (200, 404)
|
||||
|
||||
|
||||
# ── Tracking Batch ──
|
||||
|
||||
|
||||
def test_batch_track_include(client, db_session):
|
||||
"""Tests batch tracking include action sets is_ignored=0."""
|
||||
file1 = models.FilesystemState(
|
||||
file_path="/data/important.txt", size=100, mtime=1000, is_ignored=True
|
||||
)
|
||||
db_session.add(file1)
|
||||
db_session.commit()
|
||||
|
||||
response = client.post(
|
||||
"/system/track/batch",
|
||||
json={"tracks": ["/data/important.txt"], "untracks": []},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert "synchronized" in response.json()["message"]
|
||||
|
||||
db_session.expire_all()
|
||||
assert db_session.get(models.FilesystemState, file1.id).is_ignored is False
|
||||
|
||||
# Verify tracked source record was created
|
||||
ts = (
|
||||
db_session.query(models.TrackedSource)
|
||||
.filter_by(path="/data/important.txt")
|
||||
.first()
|
||||
)
|
||||
assert ts is not None
|
||||
assert ts.action == "include"
|
||||
|
||||
|
||||
def test_batch_track_exclude(client, db_session):
|
||||
"""Tests batch tracking exclude action sets is_ignored=1."""
|
||||
file1 = models.FilesystemState(
|
||||
file_path="/data/temp.txt", size=100, mtime=1000, is_ignored=False
|
||||
)
|
||||
db_session.add(file1)
|
||||
db_session.commit()
|
||||
|
||||
response = client.post(
|
||||
"/system/track/batch",
|
||||
json={"tracks": [], "untracks": ["/data/temp.txt"]},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
db_session.expire_all()
|
||||
assert db_session.get(models.FilesystemState, file1.id).is_ignored is True
|
||||
|
||||
ts = db_session.query(models.TrackedSource).filter_by(path="/data/temp.txt").first()
|
||||
assert ts is not None
|
||||
assert ts.action == "exclude"
|
||||
|
||||
|
||||
def test_batch_track_empty(client):
|
||||
"""Tests batch track with empty lists succeeds."""
|
||||
response = client.post("/system/track/batch", json={"tracks": [], "untracks": []})
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
# ── Archive Tree ──
|
||||
|
||||
|
||||
def test_archive_tree_empty(client):
|
||||
"""Tests archive tree when index is empty."""
|
||||
response = client.get("/archive/tree")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
|
||||
# ── Notifications ──
|
||||
|
||||
|
||||
def test_test_notification_invalid_url(client):
|
||||
"""Tests test notification with invalid URL returns 500."""
|
||||
response = client.post(
|
||||
"/system/notifications/test", json={"url": "not-a-valid-url"}
|
||||
)
|
||||
# Notification manager may succeed or fail depending on apprise parsing
|
||||
assert response.status_code in (200, 500)
|
||||
|
||||
Reference in New Issue
Block a user