feat: add product archive metadata store

This commit is contained in:
2026-09-12 13:04:32 +08:00
parent 0877ae73e8
commit bf24d20be9
3 changed files with 477 additions and 1 deletions
+115
View File
@@ -0,0 +1,115 @@
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
BACKEND_DIR = Path(__file__).resolve().parents[1]
if str(BACKEND_DIR) not in sys.path:
sys.path.insert(0, str(BACKEND_DIR))
from service.product_archive_store import ProductArchiveStore # noqa: E402
from service.schemas import ProductInput # noqa: E402
NOW = datetime(2026, 9, 12, 8, 0, tzinfo=timezone.utc)
def test_external_product_id_updates_one_product(tmp_path):
store = ProductArchiveStore(tmp_path / "service_products")
first = store.upsert_product(
ProductInput("external", "sku-42", "笔盒", "B-42", "黄色")
)
second = store.upsert_product(
ProductInput("external", "sku-42", "笔盒新版", "B-42", "黄色")
)
assert first.product_id == second.product_id
assert store.list_products(query="新版")[0].name == "笔盒新版"
def test_manual_product_has_internal_id_and_soft_delete_window(tmp_path):
store = ProductArchiveStore(tmp_path / "service_products")
product = store.upsert_product(
ProductInput("manual", None, "校长笔盒", "", "166 × 47 mm")
)
store.mark_pending_cleanup(product.product_id, now=NOW)
assert product.product_id.startswith("prod_")
assert store.get_product(product.product_id).status == "pending_cleanup"
assert store.get_product(product.product_id).purge_after == NOW + timedelta(days=30)
@pytest.mark.parametrize(
"product_input",
[
ProductInput("manual", None, "", "B-42", "黄色"),
ProductInput("external", None, "笔盒", "B-42", "黄色"),
ProductInput("external", "", "笔盒", "B-42", "黄色"),
],
)
def test_upsert_rejects_invalid_product_inputs(tmp_path, product_input):
store = ProductArchiveStore(tmp_path / "service_products")
with pytest.raises(ValueError):
store.upsert_product(product_input)
def test_unknown_product_operations_are_rejected(tmp_path):
store = ProductArchiveStore(tmp_path / "service_products")
with pytest.raises(ValueError):
store.get_product("prod_missing")
with pytest.raises(ValueError):
store.mark_pending_cleanup("prod_missing", now=NOW)
def test_purge_requires_due_pending_cleanup_and_lists_due_products(tmp_path):
store = ProductArchiveStore(tmp_path / "service_products")
product = store.upsert_product(ProductInput("manual", None, "待归档笔盒"))
with pytest.raises(ValueError):
store.purge_product(product.product_id, now=NOW)
store.mark_pending_cleanup(product.product_id, now=NOW)
future = NOW + timedelta(days=29)
with pytest.raises(ValueError):
store.purge_product(product.product_id, now=future)
assert store.due_product_cleanups(now=future) == []
due = NOW + timedelta(days=30)
assert [record.product_id for record in store.due_product_cleanups(now=due)] == [
product.product_id
]
assert store.purge_product(product.product_id, now=due).status == "purged"
def test_restore_removes_product_from_due_cleanup_selection(tmp_path):
store = ProductArchiveStore(tmp_path / "service_products")
product = store.upsert_product(ProductInput("manual", None, "恢复笔盒"))
store.mark_pending_cleanup(product.product_id, now=NOW)
restored = store.restore_product(product.product_id, now=NOW + timedelta(days=1))
assert restored.status == "active"
assert restored.purge_after is None
assert store.due_product_cleanups(now=NOW + timedelta(days=31)) == []
def test_version_bound_operations_reject_unknown_or_mismatched_versions(tmp_path):
store = ProductArchiveStore(tmp_path / "service_products")
first = store.upsert_product(ProductInput("manual", None, "第一个笔盒"))
second = store.upsert_product(ProductInput("manual", None, "第二个笔盒"))
version = store.create_version(first.product_id)
with pytest.raises(ValueError):
store.add_image(first.product_id, "ver_missing")
with pytest.raises(ValueError):
store.add_wordcloud_archive(first.product_id, "ver_missing")
with pytest.raises(ValueError):
store.add_image(second.product_id, version.version_id)
with pytest.raises(ValueError):
store.add_wordcloud_archive(second.product_id, version.version_id)