"""Tests for app.storage.object_store — local-filesystem object store. Exercises the async hash-dedup store against a real tmp_path on disk. All disk I/O is dispatched via ``asyncio.to_thread`` so the methods are genuinely async. """ from __future__ import annotations import hashlib from pathlib import Path from app.storage.object_store import ObjectStore # --------------------------------------------------------------------------- # put_bytes / get_path / exists # --------------------------------------------------------------------------- async def test_put_bytes_writes_file_and_exists_reports_true(tmp_path: Path): """put_bytes writes the blob; exists() then reports True; path matches key.""" store = ObjectStore(tmp_path) key = "foo/bar.txt" data = b"hello" assert await store.exists(key) is False path = await store.put_bytes(key, data) assert path == tmp_path / key assert path.exists() assert path.read_bytes() == data assert await store.exists(key) is True async def test_get_path_returns_base_plus_key(tmp_path: Path): """get_path is a pure path computation (no I/O). ``/`` in the key becomes real path segments (subdirectories). """ store = ObjectStore(tmp_path) assert store.get_path("a/b/c.bin") == tmp_path / "a" / "b" / "c.bin" assert store.get_path("flat.txt") == tmp_path / "flat.txt" async def test_put_bytes_overwrites_on_change(tmp_path: Path): """A second put_bytes with new content overwrites the file.""" store = ObjectStore(tmp_path) await store.put_bytes("k", b"v1") await store.put_bytes("k", b"v2") assert (tmp_path / "k").read_bytes() == b"v2" # --------------------------------------------------------------------------- # put_by_hash (dedup) # --------------------------------------------------------------------------- async def test_put_by_hash_returns_sha256_and_path_with_layout(tmp_path: Path): """Returns (sha256_hex, base/<2 chars>/); file content correct.""" store = ObjectStore(tmp_path) data = b"abc" expected_hash = hashlib.sha256(data).hexdigest() h, path = await store.put_by_hash(data, ext=".txt") assert h == expected_hash assert path == tmp_path / expected_hash[:2] / (expected_hash + ".txt") assert path.exists() assert path.read_bytes() == data async def test_put_by_hash_dedups_same_bytes_returns_same_path(tmp_path: Path): """put_by_hash with identical bytes twice -> same (hash, path).""" store = ObjectStore(tmp_path) data = b"identical" h1, p1 = await store.put_by_hash(data) h2, p2 = await store.put_by_hash(data) assert h1 == h2 assert p1 == p2 async def test_put_by_hash_does_not_rewrite_existing_file(tmp_path: Path): """Dedup: a second call for an existing hash must NOT touch the file. Proves the implementation checks ``exists`` before writing. We corrupt the file between calls; a dedup-aware store must leave the corruption in place (no rewrite), since the blob is already present. """ store = ObjectStore(tmp_path) data = b"original" _h1, p1 = await store.put_by_hash(data, ext=".bin") assert p1.read_bytes() == data # Corrupt the on-disk file. p1.write_bytes(b"CORRUPTED") _h2, p2 = await store.put_by_hash(data, ext=".bin") assert p2 == p1 # Dedup short-circuit: file content was NOT overwritten. assert p2.read_bytes() == b"CORRUPTED" async def test_put_by_hash_distinct_bytes_get_distinct_paths(tmp_path: Path): """Different bytes -> different hash -> different path.""" store = ObjectStore(tmp_path) h1, p1 = await store.put_by_hash(b"aaa") h2, p2 = await store.put_by_hash(b"bbb") assert h1 != h2 assert p1 != p2 async def test_put_by_hash_default_ext_empty(tmp_path: Path): """Default ext='' -> no suffix appended to the file.""" store = ObjectStore(tmp_path) data = b"x" expected_hash = hashlib.sha256(data).hexdigest() h, p = await store.put_by_hash(data) assert h == expected_hash assert p == tmp_path / expected_hash[:2] / expected_hash # --------------------------------------------------------------------------- # put_stream # --------------------------------------------------------------------------- async def test_put_stream_writes_concatenated_chunks(tmp_path: Path): """put_stream consumes an async generator and writes concatenated bytes.""" async def gen(): yield b"chunk1-" yield b"chunk2-" yield b"chunk3" store = ObjectStore(tmp_path) path = await store.put_stream("streamed/key", gen()) assert path == tmp_path / "streamed" / "key" assert path.read_bytes() == b"chunk1-chunk2-chunk3" # --------------------------------------------------------------------------- # Default base dir (Settings.media_dir) # --------------------------------------------------------------------------- def test_object_store_default_base_uses_settings_media_dir(tmp_path: Path, monkeypatch): """Omitting base_dir falls back to Settings.media_dir.""" monkeypatch.setenv("MEDIA_DIR", str(tmp_path)) from app.config import get_settings get_settings.cache_clear() store = ObjectStore() assert store.base_dir == get_settings().media_dir