Files
tapehoard/backend/tests/test_api_discrepancies.py
T
adamlamers 1dc501f50f
Continuous Integration / backend-tests (push) Successful in 32s
Continuous Integration / frontend-check (push) Successful in 17s
Continuous Integration / e2e-tests (push) Successful in 6m37s
more test coverage
2026-05-05 13:51:51 -04:00

446 lines
13 KiB
Python

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