Added some basic field CRUD tests
This commit is contained in:
@@ -38,7 +38,11 @@ class ExtraFieldParameters(BaseModel):
|
|||||||
unit: Optional[str] = Field(None, description="Unit of the value", min_length=1, max_length=16)
|
unit: Optional[str] = Field(None, description="Unit of the value", min_length=1, max_length=16)
|
||||||
field_type: ExtraFieldType = Field(description="Type of the field")
|
field_type: ExtraFieldType = Field(description="Type of the field")
|
||||||
default_value: Optional[str] = Field(None, description="Default value of the field")
|
default_value: Optional[str] = Field(None, description="Default value of the field")
|
||||||
choices: Optional[list[str]] = Field(None, description="Choices for the field, only for field type choice")
|
choices: Optional[list[str]] = Field(
|
||||||
|
None,
|
||||||
|
description="Choices for the field, only for field type choice",
|
||||||
|
min_items=1,
|
||||||
|
)
|
||||||
multi_choice: Optional[bool] = Field(None, description="Whether multiple choices can be selected")
|
multi_choice: Optional[bool] = Field(None, description="Whether multiple choices can be selected")
|
||||||
|
|
||||||
|
|
||||||
@@ -100,18 +104,12 @@ def validate_extra_field_value(field: ExtraFieldParameters, value: str) -> None:
|
|||||||
raise ValueError(f"Unknown field type {field.field_type}.")
|
raise ValueError(f"Unknown field type {field.field_type}.")
|
||||||
|
|
||||||
|
|
||||||
def validate_extra_field(field: ExtraFieldParameters) -> None: # noqa: C901
|
def validate_extra_field(field: ExtraFieldParameters) -> None:
|
||||||
"""Validate an extra field."""
|
"""Validate an extra field."""
|
||||||
# Validate choices exist if field type is choice
|
# Validate choices exist if field type is choice
|
||||||
if field.field_type == ExtraFieldType.choice:
|
if field.field_type == ExtraFieldType.choice:
|
||||||
if field.choices is None:
|
if field.choices is None:
|
||||||
raise ValueError("Choices must be set for field type choice.")
|
raise ValueError("Choices must be set for field type choice.")
|
||||||
if not isinstance(field.choices, list):
|
|
||||||
raise ValueError("Choices must be a list.")
|
|
||||||
if not all(isinstance(choice, str) for choice in field.choices):
|
|
||||||
raise ValueError("Choices must be a list of strings.")
|
|
||||||
if len(field.choices) == 0:
|
|
||||||
raise ValueError("Choices must not be empty.")
|
|
||||||
if field.multi_choice is None:
|
if field.multi_choice is None:
|
||||||
raise ValueError("Multi choice must be set for field type choice.")
|
raise ValueError("Multi choice must be set for field type choice.")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -240,3 +240,54 @@ def length_from_weight(*, weight: float, diameter: float, density: float) -> flo
|
|||||||
volume_cm3 = weight / density
|
volume_cm3 = weight / density
|
||||||
volume_mm3 = volume_cm3 * 1000
|
volume_mm3 = volume_cm3 * 1000
|
||||||
return volume_mm3 / (math.pi * (diameter / 2) ** 2)
|
return volume_mm3 / (math.pi * (diameter / 2) ** 2)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_dicts_compatible(actual: Any, expected: Any, path: str = "") -> None: # noqa: ANN401
|
||||||
|
"""Assert that two dictionaries are compatible for unit testing a REST API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
actual (dict): The actual dictionary.
|
||||||
|
expected (dict): The expected dictionary.
|
||||||
|
path (str): The path to the current level in the dictionary (used for error messages).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AssertionError: If dictionaries are not compatible.
|
||||||
|
"""
|
||||||
|
# Check if both inputs are dictionaries
|
||||||
|
if not (isinstance(actual, dict) and isinstance(expected, dict)):
|
||||||
|
raise TypeError(f"At {path}: Actual and expected values must be dictionaries.")
|
||||||
|
|
||||||
|
# Check if actual dictionary contains all keys of the expected dictionary
|
||||||
|
missing_keys = [key for key in expected if key not in actual]
|
||||||
|
if missing_keys:
|
||||||
|
raise AssertionError(f"At {path}: Missing keys in actual dictionary: {missing_keys}")
|
||||||
|
|
||||||
|
# Recursively check values if the corresponding keys exist
|
||||||
|
for key, expected_value in expected.items():
|
||||||
|
actual_value = actual[key]
|
||||||
|
subpath = f"{path}.{key}" if path else key # Update the path for the current level
|
||||||
|
|
||||||
|
# If the value is another dictionary, recurse into it
|
||||||
|
if isinstance(expected_value, dict):
|
||||||
|
assert_dicts_compatible(actual_value, expected_value, path=subpath)
|
||||||
|
elif actual_value != expected_value: # Check if values are equal
|
||||||
|
raise AssertionError(
|
||||||
|
f"At {subpath}: Values do not match. Expected: {expected_value}, Actual: {actual_value}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_lists_compatible(a: Iterable[dict[str, Any]], b: Iterable[dict[str, Any]], sort_key: str = "id") -> None:
|
||||||
|
"""Compare two lists of items where the order of the items is not guaranteed."""
|
||||||
|
a_sorted = sorted(a, key=lambda x: x[sort_key])
|
||||||
|
b_sorted = sorted(b, key=lambda x: x[sort_key])
|
||||||
|
if len(a_sorted) != len(b_sorted):
|
||||||
|
pytest.fail(f"Lists have different lengths: {len(a_sorted)} != {len(b_sorted)}")
|
||||||
|
|
||||||
|
for a_filament, b_filament in zip(a_sorted, b_sorted):
|
||||||
|
assert_dicts_compatible(a_filament, b_filament)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_httpx_success(response: httpx.Response) -> None:
|
||||||
|
"""Assert that a response is successful."""
|
||||||
|
if not response.is_success:
|
||||||
|
pytest.fail(f"Request failed: {response.status_code} {response.text}")
|
||||||
|
|||||||
1
tests_integration/tests/fields/__init__.py
Normal file
1
tests_integration/tests/fields/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Integration tests for the custom extra fields system."""
|
||||||
262
tests_integration/tests/fields/test_create.py
Normal file
262
tests_integration/tests/fields/test_create.py
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
"""Integration tests for the custom extra fields system."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ..conftest import assert_httpx_success
|
||||||
|
|
||||||
|
URL = "http://spoolman:8000"
|
||||||
|
|
||||||
|
|
||||||
|
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",
|
||||||
|
"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(timezone.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."
|
||||||
31
tests_integration/tests/fields/test_delete.py
Normal file
31
tests_integration/tests/fields/test_delete.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"""Integration tests for the custom extra fields system."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from ..conftest import assert_httpx_success
|
||||||
|
|
||||||
|
URL = "http://spoolman:8000"
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_field():
|
||||||
|
"""Test adding a field, deleting it, and making sure it's gone."""
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Delete
|
||||||
|
result = httpx.delete(f"{URL}/api/v1/field/spool/mytextfield")
|
||||||
|
assert_httpx_success(result)
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
result = httpx.get(f"{URL}/api/v1/field/spool")
|
||||||
|
assert_httpx_success(result)
|
||||||
|
assert result.json() == []
|
||||||
60
tests_integration/tests/fields/test_get.py
Normal file
60
tests_integration/tests/fields/test_get.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"""Integration tests for the custom extra fields system."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from ..conftest import assert_httpx_success, assert_lists_compatible
|
||||||
|
|
||||||
|
URL = "http://spoolman:8000"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_field():
|
||||||
|
"""Test adding a couple of fields to the spool and then getting them."""
|
||||||
|
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/myintfield",
|
||||||
|
json={
|
||||||
|
"name": "My int field",
|
||||||
|
"field_type": "integer",
|
||||||
|
"default_value": json.dumps(42),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert_httpx_success(result)
|
||||||
|
|
||||||
|
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("Hello World"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "My int field",
|
||||||
|
"key": "myintfield",
|
||||||
|
"field_type": "integer",
|
||||||
|
"default_value": json.dumps(42),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sort_key="key",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
result = httpx.delete(f"{URL}/api/v1/field/spool/mytextfield")
|
||||||
|
assert_httpx_success(result)
|
||||||
|
|
||||||
|
result = httpx.delete(f"{URL}/api/v1/field/spool/myintfield")
|
||||||
|
assert_httpx_success(result)
|
||||||
Reference in New Issue
Block a user