Files
yunke/yunke-server/tests/test_object_store.py
T

157 lines
5.2 KiB
Python
Raw Normal View History

"""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>/<hash><ext>); 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