mirror of
https://github.com/Donkie/Spoolman.git
synced 2026-10-22 01:52:17 -05:00
The declared floor was ">=3.10", but nothing enforced it: the Docker image has pinned python:3.14-slim-bookworm for amd64/arm64/armv7 for some time, and scripts/install.sh runs "uv sync" without UV_PYTHON_DOWNLOADS=0, so uv fetches a managed interpreter when the system one is too old. Every dependency declares a floor of <=3.10 with no upper cap. Raising it unlocks enum.StrEnum, datetime.UTC and typing.Self for the authentication work that follows. Consequences handled here: - uv.lock regenerated. Four pure-backport packages drop out (async-timeout, backports-asyncio-runner, exceptiongroup, tomli). - ruff target-version follows to py314, which switches on PEP 649 deferred annotations and with them a batch of previously-dormant rules. The safe automatic fixes are applied: datetime.timezone.utc -> datetime.UTC (UP017) and redundant quotes dropped from annotations (UP037). - TC001/TC002/TC003 are ignored rather than applied. They want type-only imports moved into "if TYPE_CHECKING:" blocks, which is wrong here: FastAPI resolves Annotated[AsyncSession, Depends(...)] and websocket parameters at runtime, Pydantic resolves field types at runtime, and SQLAlchemy resolves Mapped[...] at mapper-configuration time. A TYPE_CHECKING-only import is absent from the runtime namespace, so applying them breaks dependency injection. Ruff classifies all 50 as unsafe fixes for that reason. Verified: ruff check/format clean, uv lock --check clean, alembic head resolves, SQLAlchemy mappers configure with all relationships intact and lazy="selectin" preserved on the extra-field collections.
428 lines
12 KiB
Python
428 lines
12 KiB
Python
"""Integration tests for the custom extra fields system."""
|
|
|
|
import json
|
|
from datetime import UTC, datetime
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from ..conftest import URL, assert_httpx_success, assert_lists_compatible
|
|
|
|
|
|
def test_add_text_field():
|
|
"""Test adding a text field for spools."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/mytextfield",
|
|
json={
|
|
"name": "My text field",
|
|
"field_type": "text",
|
|
"default_value": json.dumps("Hello World"),
|
|
},
|
|
)
|
|
assert_httpx_success(result)
|
|
|
|
# Clean up
|
|
result = httpx.delete(f"{URL}/api/v1/field/spool/mytextfield")
|
|
assert_httpx_success(result)
|
|
|
|
|
|
def test_add_text_field_filament():
|
|
"""Test adding a text field for filaments."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/filament/mytextfield",
|
|
json={
|
|
"name": "My text field",
|
|
"field_type": "text",
|
|
"default_value": json.dumps("Hello World"),
|
|
},
|
|
)
|
|
assert_httpx_success(result)
|
|
|
|
# Clean up
|
|
result = httpx.delete(f"{URL}/api/v1/field/filament/mytextfield")
|
|
assert_httpx_success(result)
|
|
|
|
|
|
def test_add_text_field_vendor():
|
|
"""Test adding a text field for vendors."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/vendor/mytextfield",
|
|
json={
|
|
"name": "My text field",
|
|
"field_type": "text",
|
|
"default_value": json.dumps("Hello World"),
|
|
},
|
|
)
|
|
assert_httpx_success(result)
|
|
|
|
# Clean up
|
|
result = httpx.delete(f"{URL}/api/v1/field/vendor/mytextfield")
|
|
assert_httpx_success(result)
|
|
|
|
|
|
def test_add_integer_field():
|
|
"""Test adding an integer field for spools."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/myintegerfield",
|
|
json={
|
|
"name": "My integer field",
|
|
"unit": "mm",
|
|
"field_type": "integer",
|
|
"default_value": json.dumps(42),
|
|
},
|
|
)
|
|
assert_httpx_success(result)
|
|
|
|
# Clean up
|
|
result = httpx.delete(f"{URL}/api/v1/field/spool/myintegerfield")
|
|
assert_httpx_success(result)
|
|
|
|
|
|
def test_add_integer_range_field():
|
|
"""Test adding an integer range field for spools."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/myintegerrangefield",
|
|
json={
|
|
"name": "My integer range field",
|
|
"field_type": "integer_range",
|
|
"default_value": json.dumps([0, 100]),
|
|
},
|
|
)
|
|
assert_httpx_success(result)
|
|
|
|
# Clean up
|
|
result = httpx.delete(f"{URL}/api/v1/field/spool/myintegerrangefield")
|
|
assert_httpx_success(result)
|
|
|
|
|
|
def test_add_float_field():
|
|
"""Test adding a float field for spools."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/myfloatfield",
|
|
json={
|
|
"name": "My float field",
|
|
"field_type": "float",
|
|
"default_value": json.dumps(3.14),
|
|
},
|
|
)
|
|
assert_httpx_success(result)
|
|
|
|
# Clean up
|
|
result = httpx.delete(f"{URL}/api/v1/field/spool/myfloatfield")
|
|
assert_httpx_success(result)
|
|
|
|
|
|
def test_add_float_range_field():
|
|
"""Test adding a float range field for spools."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/myfloatrangefield",
|
|
json={
|
|
"name": "My float range field",
|
|
"field_type": "float_range",
|
|
"default_value": json.dumps([0.0, 1.0]),
|
|
},
|
|
)
|
|
assert_httpx_success(result)
|
|
|
|
# Clean up
|
|
result = httpx.delete(f"{URL}/api/v1/field/spool/myfloatrangefield")
|
|
assert_httpx_success(result)
|
|
|
|
|
|
def test_add_datetime_field():
|
|
"""Test adding a datetime field for spools."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/mydatetimefield",
|
|
json={
|
|
"name": "My datetime field",
|
|
"field_type": "datetime",
|
|
"default_value": json.dumps(datetime.now(UTC).isoformat()),
|
|
},
|
|
)
|
|
assert_httpx_success(result)
|
|
|
|
# Clean up
|
|
result = httpx.delete(f"{URL}/api/v1/field/spool/mydatetimefield")
|
|
assert_httpx_success(result)
|
|
|
|
|
|
def test_add_boolean_field():
|
|
"""Test adding a boolean field for spools."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/mybooleanfield",
|
|
json={
|
|
"name": "My boolean field",
|
|
"field_type": "boolean",
|
|
"default_value": json.dumps(True), # noqa: FBT003
|
|
},
|
|
)
|
|
assert_httpx_success(result)
|
|
|
|
# Clean up
|
|
result = httpx.delete(f"{URL}/api/v1/field/spool/mybooleanfield")
|
|
assert_httpx_success(result)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"multi_choice",
|
|
[True, False],
|
|
)
|
|
def test_add_choice_field(multi_choice: bool): # noqa: FBT001
|
|
"""Test adding a choice field for spools."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/mychoicefield",
|
|
json={
|
|
"name": "My choice field",
|
|
"field_type": "choice",
|
|
"choices": ["foo", "bar", "baz"],
|
|
"default_value": json.dumps(["foo"]) if multi_choice else json.dumps("foo"),
|
|
"multi_choice": multi_choice,
|
|
},
|
|
)
|
|
assert_httpx_success(result)
|
|
|
|
# Clean up
|
|
result = httpx.delete(f"{URL}/api/v1/field/spool/mychoicefield")
|
|
assert_httpx_success(result)
|
|
|
|
|
|
def test_add_text_field_invalid_data():
|
|
"""Test adding a text field with invalid default value."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/mytextfield",
|
|
json={
|
|
"name": "My text field",
|
|
"field_type": "text",
|
|
"default_value": json.dumps(42),
|
|
},
|
|
)
|
|
assert result.status_code == 400
|
|
assert result.json()["message"] == "Default value is not valid: Value is not a string."
|
|
|
|
|
|
def test_add_choice_field_without_multi_choice():
|
|
"""Test adding a choice field without multi_choice set."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/mychoicefield",
|
|
json={
|
|
"name": "My choice field",
|
|
"field_type": "choice",
|
|
"choices": ["foo", "bar", "baz"],
|
|
"default_value": json.dumps("foo"),
|
|
},
|
|
)
|
|
assert result.status_code == 400
|
|
assert result.json()["message"] == "Multi choice must be set for field type choice."
|
|
|
|
|
|
def test_add_choice_field_invalid_choices():
|
|
"""Test adding a choice field with invalid choices."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/mychoicefield",
|
|
json={
|
|
"name": "My choice field",
|
|
"field_type": "choice",
|
|
"choices": ["foo", "bar", "baz", {"foo": "bar"}],
|
|
"default_value": json.dumps(["foo"]),
|
|
"multi_choice": True,
|
|
},
|
|
)
|
|
assert result.status_code == 422
|
|
|
|
|
|
def test_add_choice_field_invalid_default_value():
|
|
"""Test adding a choice field with invalid default value."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/mychoicefield",
|
|
json={
|
|
"name": "My choice field",
|
|
"field_type": "choice",
|
|
"choices": ["foo", "bar", "baz"],
|
|
"default_value": json.dumps(42),
|
|
"multi_choice": False,
|
|
},
|
|
)
|
|
assert result.status_code == 400
|
|
assert result.json()["message"] == "Default value is not valid: Value is not a string."
|
|
|
|
|
|
def test_add_choice_field_no_choices():
|
|
"""Test adding a choice field without choices set."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/mychoicefield",
|
|
json={
|
|
"name": "My choice field",
|
|
"field_type": "choice",
|
|
"default_value": json.dumps("foo"),
|
|
"multi_choice": True,
|
|
},
|
|
)
|
|
assert result.status_code == 400
|
|
assert result.json()["message"] == "Choices must be set for field type choice."
|
|
|
|
|
|
def test_update_existing_field():
|
|
"""Test updating an existing field."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/mytextfield",
|
|
json={
|
|
"name": "My text field",
|
|
"field_type": "text",
|
|
"default_value": json.dumps("Hello World"),
|
|
},
|
|
)
|
|
assert_httpx_success(result)
|
|
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/mytextfield",
|
|
json={
|
|
"name": "My text field",
|
|
"field_type": "text",
|
|
"default_value": json.dumps("Goodbye World"),
|
|
},
|
|
)
|
|
assert_httpx_success(result)
|
|
|
|
# Verify that the default value was updated, and that it is still the only field
|
|
result = httpx.get(f"{URL}/api/v1/field/spool")
|
|
assert_httpx_success(result)
|
|
assert_lists_compatible(
|
|
result.json(),
|
|
[
|
|
{
|
|
"name": "My text field",
|
|
"key": "mytextfield",
|
|
"field_type": "text",
|
|
"default_value": json.dumps("Goodbye World"),
|
|
},
|
|
],
|
|
sort_key="key",
|
|
)
|
|
|
|
# Clean up
|
|
result = httpx.delete(f"{URL}/api/v1/field/spool/mytextfield")
|
|
assert_httpx_success(result)
|
|
|
|
|
|
def test_update_field_change_field_type():
|
|
"""Test updating an existing field and changing the field type, should not be allowed."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/mytextfield",
|
|
json={
|
|
"name": "My text field",
|
|
"field_type": "text",
|
|
"default_value": json.dumps("Hello World"),
|
|
},
|
|
)
|
|
assert_httpx_success(result)
|
|
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/mytextfield",
|
|
json={
|
|
"name": "My text field",
|
|
"field_type": "integer",
|
|
"default_value": json.dumps(42),
|
|
},
|
|
)
|
|
assert result.status_code == 400
|
|
assert result.json()["message"] == "Field type cannot be changed."
|
|
|
|
# Clean up
|
|
result = httpx.delete(f"{URL}/api/v1/field/spool/mytextfield")
|
|
assert_httpx_success(result)
|
|
|
|
|
|
def test_update_field_add_choice():
|
|
"""Test updating an existing field and adding a choice, should be allowed."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/mychoicefield",
|
|
json={
|
|
"name": "My choice field",
|
|
"field_type": "choice",
|
|
"choices": ["foo", "bar", "baz"],
|
|
"default_value": json.dumps(["foo"]),
|
|
"multi_choice": True,
|
|
},
|
|
)
|
|
assert_httpx_success(result)
|
|
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/mychoicefield",
|
|
json={
|
|
"name": "My choice field",
|
|
"field_type": "choice",
|
|
"choices": ["foo", "bar", "baz", "qux"],
|
|
"default_value": json.dumps(["foo"]),
|
|
"multi_choice": True,
|
|
},
|
|
)
|
|
assert_httpx_success(result)
|
|
|
|
# Clean up
|
|
result = httpx.delete(f"{URL}/api/v1/field/spool/mychoicefield")
|
|
assert_httpx_success(result)
|
|
|
|
|
|
def test_update_field_remove_choice():
|
|
"""Test updating an existing field and removing a choice, should not be allowed."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/mychoicefield",
|
|
json={
|
|
"name": "My choice field",
|
|
"field_type": "choice",
|
|
"choices": ["foo", "bar", "baz"],
|
|
"default_value": json.dumps(["foo"]),
|
|
"multi_choice": True,
|
|
},
|
|
)
|
|
assert_httpx_success(result)
|
|
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/mychoicefield",
|
|
json={
|
|
"name": "My choice field",
|
|
"field_type": "choice",
|
|
"choices": ["foo", "baz"],
|
|
"default_value": json.dumps(["foo"]),
|
|
"multi_choice": True,
|
|
},
|
|
)
|
|
assert result.status_code == 400
|
|
assert result.json()["message"] == "Cannot remove existing choices."
|
|
|
|
# Clean up
|
|
result = httpx.delete(f"{URL}/api/v1/field/spool/mychoicefield")
|
|
assert_httpx_success(result)
|
|
|
|
|
|
def test_update_field_change_multi_choice():
|
|
"""Test updating an existing field and changing the multi_choice setting, should not be allowed."""
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/mychoicefield",
|
|
json={
|
|
"name": "My choice field",
|
|
"field_type": "choice",
|
|
"choices": ["foo", "bar", "baz"],
|
|
"default_value": json.dumps(["foo"]),
|
|
"multi_choice": True,
|
|
},
|
|
)
|
|
assert_httpx_success(result)
|
|
|
|
result = httpx.post(
|
|
f"{URL}/api/v1/field/spool/mychoicefield",
|
|
json={
|
|
"name": "My choice field",
|
|
"field_type": "choice",
|
|
"choices": ["foo", "bar", "baz"],
|
|
"default_value": json.dumps("foo"),
|
|
"multi_choice": False,
|
|
},
|
|
)
|
|
assert result.status_code == 400
|
|
assert result.json()["message"] == "Multi choice cannot be changed."
|
|
|
|
# Clean up
|
|
result = httpx.delete(f"{URL}/api/v1/field/spool/mychoicefield")
|
|
assert_httpx_success(result)
|