Files
learningcircuit--local-deep…/tests/web/routes/test_backup_status_api.py
T
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

174 lines
6.1 KiB
Python

"""Tests for the backup status API endpoint logic."""
from local_deep_research.utilities.formatting import human_size
class TestHumanSize:
"""Tests for the shared human_size formatter."""
def test_zero_bytes(self):
assert human_size(0) == "0.0 B"
def test_bytes(self):
assert human_size(500) == "500.0 B"
def test_kilobytes(self):
assert human_size(1536) == "1.5 KB"
def test_megabytes(self):
result = human_size(258_179_072)
assert "MB" in result
assert result == "246.2 MB"
def test_gigabytes(self):
result = human_size(2_147_483_648)
assert result == "2.0 GB"
def test_terabytes(self):
result = human_size(1_099_511_627_776)
assert result == "1.0 TB"
def test_petabytes(self):
result = human_size(1_125_899_906_842_624) # 1 PB = 1024^5
assert result == "1.0 PB"
def test_exabytes_fallback(self):
result = human_size(1_152_921_504_606_846_976) # 1 EB = 1024^6
assert result == "1.0 EB"
def test_negative_petabytes(self):
result = human_size(-1_125_899_906_842_624) # -1 PB
assert result == "-1.0 PB"
class TestBackupStatusResponseShape:
"""Tests that verify backup status response structure using real filesystem."""
def test_no_backups_returns_empty(self, tmp_path):
"""When backup directory is empty, response should have count=0."""
backup_dir = tmp_path / "backups"
backup_dir.mkdir()
backups = sorted(
backup_dir.glob("ldr_backup_*.db"),
key=lambda p: p.name,
reverse=True,
)
assert len(backups) == 0
def test_single_backup_detected(self, tmp_path):
"""A single backup file should be found and sized correctly."""
backup_dir = tmp_path / "backups"
backup_dir.mkdir()
backup = backup_dir / "ldr_backup_20260326_120000.db"
backup.write_bytes(b"x" * 4096)
backups = sorted(
backup_dir.glob("ldr_backup_*.db"),
key=lambda p: p.name,
reverse=True,
)
assert len(backups) == 1
assert backups[0].stat().st_size == 4096
def test_multiple_backups_sorted_newest_first(self, tmp_path):
"""Multiple backups should sort newest first."""
backup_dir = tmp_path / "backups"
backup_dir.mkdir()
(backup_dir / "ldr_backup_20260325_120000.db").write_bytes(b"old")
(backup_dir / "ldr_backup_20260326_120000.db").write_bytes(b"new!")
backups = sorted(
backup_dir.glob("ldr_backup_*.db"),
key=lambda p: p.name,
reverse=True,
)
assert backups[0].name == "ldr_backup_20260326_120000.db"
assert backups[1].name == "ldr_backup_20260325_120000.db"
def test_tmp_files_not_included(self, tmp_path):
"""Temporary .tmp files should not appear in backup listing."""
backup_dir = tmp_path / "backups"
backup_dir.mkdir()
(backup_dir / "ldr_backup_20260326_120000.db").write_bytes(b"real")
(backup_dir / "ldr_backup_20260326_130000.db.tmp").write_bytes(b"temp")
backups = list(backup_dir.glob("ldr_backup_*.db"))
# .tmp should not match the *.db glob
assert len(backups) == 1
assert backups[0].name == "ldr_backup_20260326_120000.db"
def test_total_size_calculation(self, tmp_path):
"""Total size should sum all backup file sizes."""
backup_dir = tmp_path / "backups"
backup_dir.mkdir()
(backup_dir / "ldr_backup_20260325_120000.db").write_bytes(b"x" * 1000)
(backup_dir / "ldr_backup_20260326_120000.db").write_bytes(b"x" * 2000)
backups = list(backup_dir.glob("ldr_backup_*.db"))
total = sum(b.stat().st_size for b in backups)
assert total == 3000
class TestBackupStatusEndpointGlobHardening:
"""Real endpoint tests that drive GET /settings/api/backup-status.
Unlike the response-shape tests above (which replicate the glob), these
actually invoke the route so they verify the symlink hardening is wired
into the handler, not just that pathlib behaves.
"""
def test_symlinked_backup_entry_is_excluded(
self, authenticated_client, tmp_path
):
"""A symlink planted in the backup dir must not appear in the API
response, so an external target's metadata is never reported."""
from local_deep_research.config.paths import (
get_user_backup_directory,
)
with authenticated_client.session_transaction() as sess:
username = sess.get("username")
assert username, "authenticated_client should have a logged-in user"
backup_dir = get_user_backup_directory(username)
backup_dir.mkdir(parents=True, exist_ok=True)
# A legitimate backup.
real = backup_dir / "ldr_backup_20260101_120000.db"
real.write_bytes(b"x" * 2048)
# A malicious symlink whose name matches the glob but targets a file
# outside the backup directory.
outside = tmp_path / "outside_secret.db"
outside.write_bytes(b"secret-data-of-known-size")
evil = backup_dir / "ldr_backup_29991231_235959.db"
evil.symlink_to(outside)
resp = authenticated_client.get("/settings/api/backup-status")
assert resp.status_code == 200
data = resp.get_json()
names = [b["filename"] for b in data["backups"]]
assert "ldr_backup_20260101_120000.db" in names
assert "ldr_backup_29991231_235959.db" not in names
# The aggregate must never count more than what is listed — guards a
# future regression where an entry is filtered from the list but its
# (external) target size still leaks into total_size_bytes. Asserted as
# an invariant (not an absolute byte count) so an async post-login
# backup landing a real file in this dir can't make it flaky.
assert data["total_size_bytes"] == sum(
b["size_bytes"] for b in data["backups"]
)
# Listing must be non-destructive: the symlink and its target survive.
assert evil.is_symlink()
assert outside.exists()