from __future__ import annotations import os import uuid from contextlib import contextmanager from pathlib import Path import pytest from sqlalchemy import select, text from sqlalchemy.orm import Session from app.domains.auth.models import User, UserRole from app.domains.media.models import MediaAsset, MediaAssetType from app.domains.orders.models import Order, OrderLine, OrderStatus from app.domains.products.models import CadFile, Product from app.domains.rendering.models import OutputType from tests.db_test_utils import sync_test_session as sync_test_session_ctx @pytest.fixture def sync_session(): with sync_test_session_ctx() as session: yield session def _seed_order_line(session: Session, tmp_path: Path) -> OrderLine: step_path = tmp_path / "parts" / "bearing.step" step_path.parent.mkdir(parents=True, exist_ok=True) step_path.write_text("STEP", encoding="utf-8") user = User( id=uuid.uuid4(), email=f"publish-{uuid.uuid4().hex[:8]}@test.local", password_hash="hash", full_name="Publish Tester", role=UserRole.admin, is_active=True, ) cad_file = CadFile( id=uuid.uuid4(), original_name="bearing.step", stored_path=str(step_path), file_hash=f"hash-{uuid.uuid4().hex}", ) product = Product( id=uuid.uuid4(), pim_id="P-2000", name="Bearing Publish", category_key="bearings", cad_file_id=cad_file.id, cad_file=cad_file, ) output_type = OutputType( id=uuid.uuid4(), name="HQ Still", renderer="blender", output_format="png", render_settings={"width": 1600, "height": 900}, ) order = Order( id=uuid.uuid4(), order_number=f"ORD-{uuid.uuid4().hex[:8]}", status=OrderStatus.processing, created_by=user.id, ) line = OrderLine( id=uuid.uuid4(), order_id=order.id, product_id=product.id, product=product, output_type_id=output_type.id, output_type=output_type, render_status="processing", ) session.add_all([user, cad_file, product, output_type, order, line]) session.commit() return line def test_publish_asset_canonicalizes_still_outputs(sync_session, tmp_path, monkeypatch): from app.config import settings from app.domains.rendering.tasks import publish_asset upload_dir = tmp_path / "uploads" monkeypatch.setattr(settings, "upload_dir", str(upload_dir)) line = _seed_order_line(sync_session, tmp_path) source_output = tmp_path / "parts" / "renders" / "line.png" source_output.parent.mkdir(parents=True, exist_ok=True) source_output.write_bytes(b"png") @contextmanager def _session_ctx(): yield sync_session monkeypatch.setattr("app.core.db_utils.get_sync_session", _session_ctx) asset_id = publish_asset.run( str(line.id), "still", str(source_output), render_config={"renderer": "blender", "engine_used": "cycles"}, ) sync_session.expire_all() stored_line = sync_session.get(OrderLine, line.id) stored_asset = sync_session.execute( select(MediaAsset).where(MediaAsset.id == uuid.UUID(asset_id)) ).scalar_one() assert stored_line.result_path == f"{upload_dir}/renders/{line.id}/Bearing_Publish_HQ_Still.png" assert Path(stored_line.result_path).is_file() assert stored_asset.storage_key == f"renders/{line.id}/Bearing_Publish_HQ_Still.png" assert stored_asset.asset_type == MediaAssetType.still def test_publish_asset_canonicalizes_blend_storage_key_without_touching_order_line(sync_session, tmp_path, monkeypatch): from app.config import settings from app.domains.rendering.tasks import publish_asset upload_dir = tmp_path / "uploads" monkeypatch.setattr(settings, "upload_dir", str(upload_dir)) line = _seed_order_line(sync_session, tmp_path) source_output = tmp_path / "parts" / "bearing_production.blend" source_output.parent.mkdir(parents=True, exist_ok=True) source_output.write_bytes(b"blend") @contextmanager def _session_ctx(): yield sync_session monkeypatch.setattr("app.core.db_utils.get_sync_session", _session_ctx) asset_id = publish_asset.run( str(line.id), "blend_production", str(source_output), render_config={"artifact_type": "blend_production"}, ) sync_session.expire_all() stored_line = sync_session.get(OrderLine, line.id) stored_asset = sync_session.execute( select(MediaAsset).where(MediaAsset.id == uuid.UUID(asset_id)) ).scalar_one() assert stored_line.result_path is None assert stored_asset.storage_key == str(source_output) assert stored_asset.asset_type == MediaAssetType.blend_production