Files
Spoolman/tests_integration/tests/fields/test_values.py

161 lines
7.3 KiB
Python

"""Tests for the extra-field distinct-values endpoint (GET /field/{entity}/{key}/values)."""
import json
import uuid
from typing import Any
import httpx
import pytest
from ..conftest import URL, assert_httpx_success
def _create_entity(entity_type: str, extra: dict[str, str], random_filament: dict[str, Any]) -> int:
"""Create a test entity of the given type with the given extra fields. Returns the entity id."""
if entity_type == "spool":
result = httpx.post(
f"{URL}/api/v1/spool",
json={"filament_id": random_filament["id"], "extra": extra},
)
elif entity_type == "filament":
result = httpx.post(
f"{URL}/api/v1/filament",
json={
"vendor_id": random_filament["vendor"]["id"],
"name": f"Test-{uuid.uuid4().hex[:8]}",
"density": 1.24,
"diameter": 1.75,
"extra": extra,
},
)
elif entity_type == "vendor":
result = httpx.post(
f"{URL}/api/v1/vendor",
json={"name": f"Vendor-{uuid.uuid4().hex[:8]}", "extra": extra},
)
else:
raise ValueError(f"Unknown entity type: {entity_type}")
result.raise_for_status()
return result.json()["id"]
@pytest.mark.asyncio
@pytest.mark.parametrize("entity_type", ["spool", "filament", "vendor"])
async def test_text_field_values_are_distinct_and_sorted(entity_type: str, random_filament: dict[str, Any]) -> None:
"""The values endpoint returns the distinct, sorted, in-use values of a text field."""
field_key = f"vals_text_{uuid.uuid4().hex[:8]}"
httpx.post(
f"{URL}/api/v1/field/{entity_type}/{field_key}",
json={"name": "Values text", "field_type": "text"},
).raise_for_status()
# Two entities share "A1"; the endpoint must de-duplicate. One uses "B2".
id1 = _create_entity(entity_type, {field_key: json.dumps("A1")}, random_filament)
id2 = _create_entity(entity_type, {field_key: json.dumps("B2")}, random_filament)
id3 = _create_entity(entity_type, {field_key: json.dumps("A1")}, random_filament)
try:
result = httpx.get(f"{URL}/api/v1/field/{entity_type}/{field_key}/values")
assert_httpx_success(result)
# The field key is unique to this test, so the values are exactly what we set.
assert result.json() == ["A1", "B2"]
finally:
httpx.delete(f"{URL}/api/v1/field/{entity_type}/{field_key}").raise_for_status()
for eid in (id1, id2, id3):
httpx.delete(f"{URL}/api/v1/{entity_type}/{eid}").raise_for_status()
@pytest.mark.asyncio
@pytest.mark.parametrize("entity_type", ["spool", "filament", "vendor"])
async def test_values_exclude_unset_and_empty(entity_type: str, random_filament: dict[str, Any]) -> None:
"""Entities with no value, or an explicitly empty value, do not contribute to the values list."""
field_key = f"vals_opt_{uuid.uuid4().hex[:8]}"
httpx.post(
f"{URL}/api/v1/field/{entity_type}/{field_key}",
json={"name": "Values optional", "field_type": "text"},
).raise_for_status()
id_set = _create_entity(entity_type, {field_key: json.dumps("here")}, random_filament)
id_empty = _create_entity(entity_type, {field_key: json.dumps("")}, random_filament)
id_unset = _create_entity(entity_type, {}, random_filament)
try:
result = httpx.get(f"{URL}/api/v1/field/{entity_type}/{field_key}/values")
assert_httpx_success(result)
assert result.json() == ["here"]
finally:
httpx.delete(f"{URL}/api/v1/field/{entity_type}/{field_key}").raise_for_status()
for eid in (id_set, id_empty, id_unset):
httpx.delete(f"{URL}/api/v1/{entity_type}/{eid}").raise_for_status()
@pytest.mark.asyncio
@pytest.mark.parametrize("entity_type", ["spool", "filament", "vendor"])
async def test_single_choice_field_values(entity_type: str, random_filament: dict[str, Any]) -> None:
"""The values endpoint returns the in-use choices of a single-choice field (not all defined choices)."""
field_key = f"vals_choice_{uuid.uuid4().hex[:8]}"
httpx.post(
f"{URL}/api/v1/field/{entity_type}/{field_key}",
json={
"name": "Values choice",
"field_type": "choice",
"choices": ["Good", "Bad", "Meh"],
"multi_choice": False,
},
).raise_for_status()
# Only "Good" and "Bad" are actually used; "Meh" is defined but unused.
id1 = _create_entity(entity_type, {field_key: json.dumps("Good")}, random_filament)
id2 = _create_entity(entity_type, {field_key: json.dumps("Bad")}, random_filament)
try:
result = httpx.get(f"{URL}/api/v1/field/{entity_type}/{field_key}/values")
assert_httpx_success(result)
assert result.json() == ["Bad", "Good"]
finally:
httpx.delete(f"{URL}/api/v1/field/{entity_type}/{field_key}").raise_for_status()
httpx.delete(f"{URL}/api/v1/{entity_type}/{id1}").raise_for_status()
httpx.delete(f"{URL}/api/v1/{entity_type}/{id2}").raise_for_status()
@pytest.mark.asyncio
@pytest.mark.parametrize("entity_type", ["spool", "filament", "vendor"])
async def test_values_non_ascii(entity_type: str, random_filament: dict[str, Any]) -> None:
"""The values endpoint decodes stored JSON, so non-ASCII values round-trip (encoding canary)."""
field_key = f"vals_unicode_{uuid.uuid4().hex[:8]}"
httpx.post(
f"{URL}/api/v1/field/{entity_type}/{field_key}",
json={"name": "Values unicode", "field_type": "text"},
).raise_for_status()
# Store unescaped, the way the JS frontend's JSON.stringify does.
id1 = _create_entity(entity_type, {field_key: json.dumps("Café åäö", ensure_ascii=False)}, random_filament)
id2 = _create_entity(entity_type, {field_key: json.dumps("日本", ensure_ascii=False)}, random_filament)
try:
result = httpx.get(f"{URL}/api/v1/field/{entity_type}/{field_key}/values")
assert_httpx_success(result)
assert set(result.json()) == {"Café åäö", "日本"}
finally:
httpx.delete(f"{URL}/api/v1/field/{entity_type}/{field_key}").raise_for_status()
httpx.delete(f"{URL}/api/v1/{entity_type}/{id1}").raise_for_status()
httpx.delete(f"{URL}/api/v1/{entity_type}/{id2}").raise_for_status()
@pytest.mark.asyncio
@pytest.mark.parametrize("entity_type", ["spool", "filament", "vendor"])
async def test_values_unknown_field_returns_404(entity_type: str) -> None:
"""Requesting values for a field that does not exist returns 404."""
result = httpx.get(f"{URL}/api/v1/field/{entity_type}/does_not_exist/values")
assert result.status_code == 404
assert "does_not_exist" in result.json()["message"]
@pytest.mark.asyncio
@pytest.mark.parametrize("entity_type", ["spool", "filament", "vendor"])
async def test_values_empty_when_no_entities(entity_type: str) -> None:
"""A defined field with no values stored anywhere returns an empty list (not an error)."""
field_key = f"vals_none_{uuid.uuid4().hex[:8]}"
httpx.post(
f"{URL}/api/v1/field/{entity_type}/{field_key}",
json={"name": "Values none", "field_type": "text"},
).raise_for_status()
try:
result = httpx.get(f"{URL}/api/v1/field/{entity_type}/{field_key}/values")
assert_httpx_success(result)
assert result.json() == []
finally:
httpx.delete(f"{URL}/api/v1/field/{entity_type}/{field_key}").raise_for_status()