"""对象存储适配器单元测试(仅 local 后端,不依赖网络)。""" from pathlib import Path from app.storage import (LocalStorage, get_storage, parse_ref, resolve_display_path, reset_storage) def test_local_save_read_delete(tmp_path): storage = LocalStorage(root=tmp_path) ref = storage.save("artifacts/seg_1.md", "正文内容".encode()) assert ref == "local://artifacts/seg_1.md" assert storage.read("artifacts/seg_1.md") == "正文内容".encode() assert storage.exists("artifacts/seg_1.md") storage.delete("artifacts/seg_1.md") assert not storage.exists("artifacts/seg_1.md") def test_local_path_traversal_blocked(tmp_path): storage = LocalStorage(root=tmp_path) ref = storage.save("../../etc/passwd", b"x") # key 被净化:引用不含 ..,文件落在 root 内而不是系统目录 assert ".." not in ref assert "etc/passwd" in ref # save 后文件真实存在于 root 内(.. 变成 _ 目录) assert not ref.startswith("local://../") saved = tmp_path / ref.removeprefix("local://") assert saved.exists() and saved.parent.is_relative_to(tmp_path) def test_parse_ref_schemes(): assert parse_ref("local://a/b.md") == ("local", "a/b.md") assert parse_ref("s3://bucket/a/b.md") == ("s3", "bucket/a/b.md") assert parse_ref("plain/path.md") == ("local", "plain/path.md") assert parse_ref("") == ("local", "") def test_resolve_display_path(tmp_path): reset_storage() local = resolve_display_path("local://artifacts/x.md") assert local.endswith("artifacts/x.md") assert resolve_display_path("s3://bucket/x.md") == "s3://bucket/x.md" def test_get_storage_default_local(tmp_path, monkeypatch): monkeypatch.delenv("YAWEN_STORAGE_BACKEND", raising=False) reset_storage() storage = get_storage() assert isinstance(storage, LocalStorage) def test_local_atomic_write(tmp_path): storage = LocalStorage(root=tmp_path) storage.save("k/a.txt", b"v1") storage.save("k/a.txt", b"v2") # 覆盖写也走 tmp+rename assert storage.read("k/a.txt") == b"v2" leftovers = [p.name for p in (tmp_path / "k").iterdir()] assert leftovers == ["a.txt"] # 无 .tmp 残留