Files
bambuddy/backend/tests/unit/test_temperature_fan_presets.py
maziggy b04664c64a Raise the chamber-temperature ceiling from 60 to 65 C
Every field that takes a chamber target stopped at 60: the per-filament
chamber map and per-print override in Preheat & Heat Soak, the chamber
quick-select presets, and the printer-card chamber control. 60 is the
X1E's ceiling and the X1E was the only heated-chamber model when that
limit was written; the H2 series and X2D heat to 65, so the top of their
range was unreachable.

The ceiling now lives in one constant per side (MAX_CHAMBER_TEMP_C in
backend/app/utils/printer_models.py and frontend/src/utils/printer.ts)
rather than as a literal at each call site. X1E firmware clamps a higher
request to its own maximum, so a shared ceiling is safe.

Also fixes a live bug at PrintersPage.tsx:7985: parsePresetTriple was
bounded to 60 there, and it rejects the whole triple on any out-of-range
entry, so a saved 65 preset would have silently reverted the printer
card to the defaults while Settings still showed 65.
2026-08-03 08:52:23 +02:00

100 lines
3.8 KiB
Python

"""Validator tests for the user-configurable temperature / fan presets."""
import pytest
from pydantic import ValidationError
from backend.app.schemas.settings import AppSettingsUpdate
# (field_name, valid_payload, range_lo, range_hi)
PRESET_FIELDS = [
("nozzle_temp_presets", "[120, 220, 260]", 0, 320),
("bed_temp_presets", "[55, 75, 90]", 0, 140),
("chamber_temp_presets", "[35, 45, 60]", 0, 65),
("fan_speed_presets", "[50, 75, 100]", 0, 100),
]
@pytest.mark.parametrize("field,valid,lo,hi", PRESET_FIELDS)
def test_valid_triple_round_trips(field, valid, lo, hi):
update = AppSettingsUpdate(**{field: valid})
assert getattr(update, field) == valid
@pytest.mark.parametrize("field,_valid,_lo,_hi", PRESET_FIELDS)
def test_empty_string_means_use_defaults(field, _valid, _lo, _hi):
update = AppSettingsUpdate(**{field: ""})
assert getattr(update, field) == ""
@pytest.mark.parametrize("field,_valid,_lo,_hi", PRESET_FIELDS)
def test_missing_field_is_optional(field, _valid, _lo, _hi):
# Updates are PATCH-style; omitting the field shouldn't trigger the validator.
update = AppSettingsUpdate()
assert getattr(update, field) is None
@pytest.mark.parametrize("field,_valid,_lo,_hi", PRESET_FIELDS)
def test_malformed_json_rejected(field, _valid, _lo, _hi):
with pytest.raises(ValidationError) as exc:
AppSettingsUpdate(**{field: "not json"})
assert field in str(exc.value)
@pytest.mark.parametrize("field,_valid,_lo,_hi", PRESET_FIELDS)
def test_non_array_rejected(field, _valid, _lo, _hi):
with pytest.raises(ValidationError) as exc:
AppSettingsUpdate(**{field: '{"a": 1}'})
assert "array of exactly 3 integers" in str(exc.value)
@pytest.mark.parametrize("field,_valid,_lo,_hi", PRESET_FIELDS)
@pytest.mark.parametrize("bad_payload", ["[]", "[100]", "[100, 200]", "[100, 200, 300, 400]"])
def test_wrong_length_rejected(field, _valid, _lo, _hi, bad_payload):
with pytest.raises(ValidationError) as exc:
AppSettingsUpdate(**{field: bad_payload})
assert "array of exactly 3 integers" in str(exc.value)
@pytest.mark.parametrize("field,_valid,_lo,_hi", PRESET_FIELDS)
def test_float_entries_rejected(field, _valid, _lo, _hi):
with pytest.raises(ValidationError) as exc:
AppSettingsUpdate(**{field: "[1.5, 2.0, 3.0]"})
assert "must all be integers" in str(exc.value)
@pytest.mark.parametrize("field,_valid,_lo,_hi", PRESET_FIELDS)
def test_string_entries_rejected(field, _valid, _lo, _hi):
with pytest.raises(ValidationError) as exc:
AppSettingsUpdate(**{field: '["120", "220", "260"]'})
assert "must all be integers" in str(exc.value)
@pytest.mark.parametrize("field,_valid,lo,hi", PRESET_FIELDS)
def test_below_range_rejected(field, _valid, lo, hi):
bad = f"[{lo - 1}, {lo}, {lo}]"
with pytest.raises(ValidationError) as exc:
AppSettingsUpdate(**{field: bad})
assert f"[{lo}, {hi}]" in str(exc.value)
@pytest.mark.parametrize("field,_valid,lo,hi", PRESET_FIELDS)
def test_above_range_rejected(field, _valid, lo, hi):
bad = f"[{hi}, {hi}, {hi + 1}]"
with pytest.raises(ValidationError) as exc:
AppSettingsUpdate(**{field: bad})
assert f"[{lo}, {hi}]" in str(exc.value)
@pytest.mark.parametrize("field,_valid,lo,hi", PRESET_FIELDS)
def test_range_bounds_inclusive(field, _valid, lo, hi):
"""Both endpoints lo and hi must be accepted (inclusive bounds)."""
update = AppSettingsUpdate(**{field: f"[{lo}, {hi}, {hi}]"})
assert getattr(update, field) == f"[{lo}, {hi}, {hi}]"
def test_booleans_rejected_even_though_python_treats_them_as_ints():
"""`isinstance(True, int)` is True in Python — explicit guard required."""
with pytest.raises(ValidationError) as exc:
AppSettingsUpdate(nozzle_temp_presets="[true, 220, 260]")
assert "must all be integers" in str(exc.value)