mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
203 lines
7.3 KiB
Python
203 lines
7.3 KiB
Python
"""Unit tests for bulk spool creation.
|
|
|
|
Tests:
|
|
- SpoolBulkCreate schema validation (quantity bounds)
|
|
- Bulk create endpoint creates the requested number of spools
|
|
- Bulk create with quantity=1 (single spool)
|
|
- Bulk create returns spools with k_profiles loaded
|
|
- Bulk create copies the inherited supplier links onto every copy
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from backend.app.models.spool import Spool
|
|
from backend.app.models.supplier import SpoolSupplier
|
|
from backend.app.schemas.spool import SpoolBulkCreate, SpoolCreate
|
|
|
|
# ── Schema Validation ──────────────────────────────────────────────────────
|
|
|
|
|
|
class TestSpoolBulkCreateSchema:
|
|
"""Tests for the SpoolBulkCreate Pydantic model."""
|
|
|
|
def test_default_quantity_is_1(self):
|
|
data = SpoolBulkCreate(spool=SpoolCreate(material="PLA"))
|
|
assert data.quantity == 1
|
|
|
|
def test_quantity_within_range(self):
|
|
data = SpoolBulkCreate(spool=SpoolCreate(material="PLA"), quantity=50)
|
|
assert data.quantity == 50
|
|
|
|
def test_quantity_max_100(self):
|
|
data = SpoolBulkCreate(spool=SpoolCreate(material="PLA"), quantity=100)
|
|
assert data.quantity == 100
|
|
|
|
def test_quantity_zero_rejected(self):
|
|
with pytest.raises(ValidationError, match="greater than or equal to 1"):
|
|
SpoolBulkCreate(spool=SpoolCreate(material="PLA"), quantity=0)
|
|
|
|
def test_quantity_negative_rejected(self):
|
|
with pytest.raises(ValidationError, match="greater than or equal to 1"):
|
|
SpoolBulkCreate(spool=SpoolCreate(material="PLA"), quantity=-1)
|
|
|
|
def test_quantity_over_100_rejected(self):
|
|
with pytest.raises(ValidationError, match="less than or equal to 100"):
|
|
SpoolBulkCreate(spool=SpoolCreate(material="PLA"), quantity=101)
|
|
|
|
def test_spool_fields_preserved(self):
|
|
data = SpoolBulkCreate(
|
|
spool=SpoolCreate(
|
|
material="PETG",
|
|
brand="Polymaker",
|
|
subtype="Basic",
|
|
color_name="Red",
|
|
rgba="FF0000FF",
|
|
label_weight=750,
|
|
note="Test batch",
|
|
),
|
|
quantity=5,
|
|
)
|
|
assert data.spool.material == "PETG"
|
|
assert data.spool.brand == "Polymaker"
|
|
assert data.spool.label_weight == 750
|
|
assert data.spool.note == "Test batch"
|
|
assert data.quantity == 5
|
|
|
|
def test_spool_without_slicer_filament_is_stock(self):
|
|
"""A spool without slicer_filament is a 'stock' spool (computed, not stored)."""
|
|
data = SpoolBulkCreate(
|
|
spool=SpoolCreate(material="PLA", label_weight=1000),
|
|
quantity=3,
|
|
)
|
|
assert data.spool.slicer_filament is None
|
|
|
|
def test_spool_with_slicer_filament_is_configured(self):
|
|
data = SpoolBulkCreate(
|
|
spool=SpoolCreate(material="PLA", slicer_filament="GFL99"),
|
|
quantity=2,
|
|
)
|
|
assert data.spool.slicer_filament == "GFL99"
|
|
|
|
def test_material_required(self):
|
|
with pytest.raises(ValidationError):
|
|
SpoolBulkCreate(spool=SpoolCreate(material=""), quantity=1)
|
|
|
|
|
|
# ── Endpoint Logic ─────────────────────────────────────────────────────────
|
|
|
|
|
|
def _make_mock_spool(spool_id):
|
|
"""Create a mock Spool ORM object."""
|
|
spool = MagicMock()
|
|
spool.id = spool_id
|
|
spool.material = "PLA"
|
|
spool.label_weight = 1000
|
|
spool.k_profiles = []
|
|
return spool
|
|
|
|
|
|
def _result(rows):
|
|
"""A stand-in for the Result of a query that selected ``rows``."""
|
|
result = MagicMock()
|
|
result.scalars.return_value.all.return_value = list(rows)
|
|
result.scalars.return_value.first.return_value = rows[0] if rows else None
|
|
result.first.return_value = (rows[0],) if rows else None
|
|
return result
|
|
|
|
|
|
def _make_db(refetched, *, donor_id=None, donor_links=()):
|
|
"""Session double that answers each of the bulk path's queries separately.
|
|
|
|
The endpoint runs three different selects — the supplier donor lookup,
|
|
that donor's link rows (#2988), and the re-fetch of the created spools —
|
|
so one canned result for all of them hands Spool rows to the supplier
|
|
code and invents a donor that was never seeded. Route on what each select
|
|
asks for instead. Returns the session and the list ``db.add`` lands in.
|
|
"""
|
|
added = []
|
|
db = AsyncMock()
|
|
db.add = added.append
|
|
|
|
async def execute(statement, *_args, **_kwargs):
|
|
described = statement.column_descriptions[0]
|
|
if described["entity"] is SpoolSupplier:
|
|
return _result(donor_links)
|
|
if described["name"] == "id":
|
|
return _result([] if donor_id is None else [donor_id])
|
|
return _result(refetched)
|
|
|
|
db.execute = AsyncMock(side_effect=execute)
|
|
return db, added
|
|
|
|
|
|
class TestBulkCreateEndpoint:
|
|
"""Tests for the bulk_create_spools endpoint logic."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_creates_requested_number_of_spools(self):
|
|
"""Verify N spools are created and added to the session."""
|
|
from backend.app.api.routes.inventory import bulk_create_spools
|
|
|
|
data = SpoolBulkCreate(
|
|
spool=SpoolCreate(material="PLA", brand="Test", label_weight=1000),
|
|
quantity=3,
|
|
)
|
|
|
|
db, added_objects = _make_db([_make_mock_spool(i + 1) for i in range(3)])
|
|
|
|
result = await bulk_create_spools(data=data, db=db, _=None)
|
|
|
|
assert len(result) == 3
|
|
assert len(added_objects) == 3
|
|
db.commit.assert_awaited_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_single_quantity_creates_one_spool(self):
|
|
"""Bulk create with quantity=1 should create exactly one spool."""
|
|
from backend.app.api.routes.inventory import bulk_create_spools
|
|
|
|
data = SpoolBulkCreate(
|
|
spool=SpoolCreate(material="PETG"),
|
|
quantity=1,
|
|
)
|
|
|
|
db, added_objects = _make_db([_make_mock_spool(1)])
|
|
|
|
result = await bulk_create_spools(data=data, db=db, _=None)
|
|
|
|
assert len(result) == 1
|
|
assert len(added_objects) == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_all_spools_have_same_fields(self):
|
|
"""All created spools should have identical field values."""
|
|
from backend.app.api.routes.inventory import bulk_create_spools
|
|
|
|
data = SpoolBulkCreate(
|
|
spool=SpoolCreate(
|
|
material="ABS",
|
|
brand="Bambu Lab",
|
|
color_name="Black",
|
|
rgba="000000FF",
|
|
label_weight=750,
|
|
),
|
|
quantity=3,
|
|
)
|
|
|
|
db, added_objects = _make_db([_make_mock_spool(i + 1) for i in range(3)])
|
|
|
|
await bulk_create_spools(data=data, db=db, _=None)
|
|
|
|
# The spools, not everything the session was handed: the bulk path also
|
|
# adds the inherited supplier links, and a SpoolSupplier has no material.
|
|
spools = [obj for obj in added_objects if isinstance(obj, Spool)]
|
|
assert len(spools) == 3
|
|
for spool_obj in spools:
|
|
assert spool_obj.material == "ABS"
|
|
assert spool_obj.brand == "Bambu Lab"
|
|
assert spool_obj.color_name == "Black"
|
|
assert spool_obj.label_weight == 750
|