mirror of
https://github.com/Donkie/Spoolman.git
synced 2026-10-22 01:52:17 -05:00
161 lines
7.3 KiB
Python
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()
|