- Fase A: license templates, search history, cost estimator - Fase B: import URL, bulk ZIP, batch download - Fase C: comparison mode, mesh validation, measurement tool - Fase D: cross-section clipping, overhang heatmap, layer animation - Refactor Pydantic/SQLAlchemy warnings - 24 tests pytest - README actualizado - WebP thumbnails, lazy loading, cache headers
275 lines
7.5 KiB
Python
275 lines
7.5 KiB
Python
import os
|
|
import tempfile
|
|
import zipfile
|
|
from unittest.mock import patch, AsyncMock
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
from sqlalchemy.pool import StaticPool
|
|
|
|
from app.database import Base, get_db
|
|
from app.main import app
|
|
|
|
engine = create_engine(
|
|
"sqlite:///:memory:",
|
|
connect_args={"check_same_thread": False},
|
|
poolclass=StaticPool,
|
|
)
|
|
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
|
|
def override_get_db():
|
|
db = TestingSessionLocal()
|
|
try:
|
|
yield db
|
|
finally:
|
|
db.close()
|
|
|
|
app.dependency_overrides[get_db] = override_get_db
|
|
Base.metadata.create_all(bind=engine)
|
|
client = TestClient(app)
|
|
|
|
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
PROJECT_DIR = os.path.dirname(TEST_DIR)
|
|
|
|
_upload_counter = 0
|
|
|
|
|
|
def _get_unique_stl():
|
|
global _upload_counter
|
|
_upload_counter += 1
|
|
fd, path = tempfile.mkstemp(suffix='.stl')
|
|
with os.fdopen(fd, 'w') as f:
|
|
f.write(f"""solid test{_upload_counter}
|
|
facet normal 0 0 1
|
|
outer loop
|
|
vertex 0 0 0
|
|
vertex {_upload_counter} 0 0
|
|
vertex {_upload_counter} {_upload_counter} 0
|
|
endloop
|
|
endfacet
|
|
endsolid test{_upload_counter}
|
|
""")
|
|
return path
|
|
|
|
|
|
def test_list_empty_models():
|
|
response = client.get("/api/models/")
|
|
assert response.status_code == 200
|
|
assert response.json() == []
|
|
|
|
|
|
def test_list_tags_empty():
|
|
response = client.get("/api/models/tags")
|
|
assert response.status_code == 200
|
|
assert response.json() == []
|
|
|
|
|
|
def _upload():
|
|
stl_path = _get_unique_stl()
|
|
with open(stl_path, 'rb') as f:
|
|
response = client.post(
|
|
"/api/models/",
|
|
data={
|
|
"title": "Test Cube",
|
|
"description": "A test cube",
|
|
"author": "Tester",
|
|
"tags": "test, cube",
|
|
"category": "Piezas"
|
|
},
|
|
files={"files": (os.path.basename(stl_path), f, "application/octet-stream")}
|
|
)
|
|
assert response.status_code == 200
|
|
return response.json()['id']
|
|
|
|
|
|
def test_upload_model():
|
|
model_id = _upload()
|
|
assert isinstance(model_id, int)
|
|
|
|
|
|
def test_get_model():
|
|
model_id = _upload()
|
|
response = client.get(f"/api/models/{model_id}")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data['id'] == model_id
|
|
assert data['title'] == "Test Cube"
|
|
|
|
|
|
def test_update_model():
|
|
model_id = _upload()
|
|
response = client.put(
|
|
f"/api/models/{model_id}",
|
|
json={"title": "Updated Cube", "tag_names": ["updated"]}
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data['title'] == "Updated Cube"
|
|
assert len(data['tags']) == 1
|
|
|
|
|
|
def test_download_model():
|
|
model_id = _upload()
|
|
response = client.get(f"/api/models/{model_id}/download")
|
|
assert response.status_code == 200
|
|
|
|
|
|
def test_thumbnail():
|
|
model_id = _upload()
|
|
response = client.get(f"/api/models/{model_id}/thumbnail")
|
|
assert response.status_code == 200
|
|
|
|
|
|
def test_qr_endpoint():
|
|
model_id = _upload()
|
|
response = client.get(f"/api/models/{model_id}/qr")
|
|
assert response.status_code == 200
|
|
|
|
|
|
def test_pagination():
|
|
response = client.get("/api/models/?skip=0&limit=10")
|
|
assert response.status_code == 200
|
|
|
|
|
|
def test_search_filter():
|
|
response = client.get("/api/models/?search=Cube")
|
|
assert response.status_code == 200
|
|
|
|
|
|
def test_delete_model():
|
|
model_id = _upload()
|
|
response = client.delete(f"/api/models/{model_id}")
|
|
assert response.status_code == 200
|
|
response = client.get(f"/api/models/{model_id}")
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_backup_endpoint():
|
|
response = client.get("/api/models/system/backup")
|
|
assert response.status_code == 200
|
|
|
|
|
|
# Phase 5: Social features
|
|
|
|
def test_create_rating():
|
|
model_id = _upload()
|
|
response = client.post(f"/api/models/{model_id}/ratings?stars=5")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data['stars'] == 5
|
|
|
|
response = client.get(f"/api/models/{model_id}")
|
|
assert response.json()['avg_rating'] == 5.0
|
|
|
|
|
|
def test_create_comment():
|
|
model_id = _upload()
|
|
response = client.post(f"/api/models/{model_id}/comments?text=Great%20model&author_name=User1")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data['text'] == "Great model"
|
|
assert data['author_name'] == "User1"
|
|
|
|
|
|
def test_list_comments():
|
|
model_id = _upload()
|
|
client.post(f"/api/models/{model_id}/comments?text=Comment1")
|
|
client.post(f"/api/models/{model_id}/comments?text=Comment2")
|
|
response = client.get(f"/api/models/{model_id}/comments")
|
|
assert response.status_code == 200
|
|
assert len(response.json()) == 2
|
|
|
|
|
|
def _create_collection():
|
|
response = client.post("/api/models/collections", json={"name": "My Collection", "description": "Test"})
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data['name'] == "My Collection"
|
|
return data['id']
|
|
|
|
|
|
def test_add_to_collection():
|
|
model_id = _upload()
|
|
coll_id = _create_collection()
|
|
response = client.post(f"/api/models/collections/{coll_id}/add/{model_id}")
|
|
assert response.status_code == 200
|
|
|
|
response = client.get(f"/api/models/collections/{coll_id}")
|
|
assert response.status_code == 200
|
|
assert len(response.json()['models']) == 1
|
|
|
|
|
|
# Phase 6: Import / Bulk / Batch / Validation / Estimate
|
|
|
|
def test_import_from_url():
|
|
stl_path = _get_unique_stl()
|
|
with open(stl_path, 'rb') as f:
|
|
stl_bytes = f.read()
|
|
|
|
from unittest.mock import Mock
|
|
mock_response = Mock()
|
|
mock_response.content = stl_bytes
|
|
mock_response.raise_for_status = Mock()
|
|
|
|
async_mock_client = AsyncMock()
|
|
async_mock_client.get = AsyncMock(return_value=mock_response)
|
|
|
|
with patch('app.routers.models.httpx.AsyncClient', return_value=async_mock_client):
|
|
async_mock_client.__aenter__ = AsyncMock(return_value=async_mock_client)
|
|
async_mock_client.__aexit__ = AsyncMock(return_value=False)
|
|
response = client.post(
|
|
"/api/models/import-url",
|
|
data={"url": "http://example.com/model.stl", "title": "Imported Model"}
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data['title'] == "Imported Model"
|
|
|
|
|
|
def test_bulk_zip_upload():
|
|
stl_path = _get_unique_stl()
|
|
zip_path = tempfile.mktemp(suffix='.zip')
|
|
with zipfile.ZipFile(zip_path, 'w') as zf:
|
|
zf.write(stl_path, os.path.basename(stl_path))
|
|
|
|
with open(zip_path, 'rb') as f:
|
|
response = client.post(
|
|
"/api/models/bulk-zip",
|
|
data={"author": "BulkTester"},
|
|
files={"zip_file": ("models.zip", f, "application/zip")}
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data) >= 1
|
|
os.remove(zip_path)
|
|
|
|
|
|
def test_batch_download():
|
|
model_id1 = _upload()
|
|
model_id2 = _upload()
|
|
response = client.post(
|
|
"/api/models/batch-download",
|
|
json=[model_id1, model_id2]
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.headers['content-type'] == 'application/zip'
|
|
|
|
|
|
def test_validate_mesh():
|
|
model_id = _upload()
|
|
response = client.get(f"/api/models/{model_id}/validate")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert 'is_watertight' in data
|
|
assert 'volume_cm3' in data
|
|
|
|
|
|
def test_estimate_print():
|
|
model_id = _upload()
|
|
response = client.get(f"/api/models/{model_id}/estimate")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert 'volume_cm3' in data
|
|
assert 'cost' in data
|
|
assert 'grams' in data
|