Merge pull request #709 from dylanlangston/master

Fix: Nutrition.calories field rejects integer values from Nextcloud Cookbook API
This commit is contained in:
Chris Coutinho
2026-04-15 12:22:06 +02:00
committed by GitHub
2 changed files with 105 additions and 1 deletions
+31 -1
View File
@@ -2,7 +2,7 @@
from typing import List, Optional, Union
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field, field_validator
from .base import BaseResponse, IdResponse, StatusResponse
@@ -40,6 +40,36 @@ class Nutrition(BaseModel):
model_config = ConfigDict(populate_by_name=True)
@field_validator(
"calories",
"carbohydrateContent",
"cholesterolContent",
"fatContent",
"fiberContent",
"proteinContent",
"saturatedFatContent",
"servingSize",
"sodiumContent",
"sugarContent",
"transFatContent",
"unsaturatedFatContent",
mode="before",
)
@classmethod
def coerce_to_str(cls, v: object) -> object:
"""Coerce numeric values to strings.
The schema.org/NutritionInformation spec allows values as either
strings (e.g. '650 kcal') or numbers (e.g. 650). Nextcloud Cookbook
stores whatever the source provided.
"""
if isinstance(v, bool):
msg = "boolean values are not valid for nutrition fields"
raise ValueError(msg)
if isinstance(v, (int, float)):
return str(v)
return v
class RecipeStub(BaseModel):
"""Stub of a recipe with basic information."""
+74
View File
@@ -0,0 +1,74 @@
"""Unit tests for Nutrition model numeric coercion (issue #708)."""
import pytest
from pydantic import ValidationError
from nextcloud_mcp_server.models.cookbook import Nutrition
NUTRITION_FIELDS = [
"calories",
"carbohydrateContent",
"cholesterolContent",
"fatContent",
"fiberContent",
"proteinContent",
"saturatedFatContent",
"servingSize",
"sodiumContent",
"sugarContent",
"transFatContent",
"unsaturatedFatContent",
]
@pytest.mark.unit
@pytest.mark.parametrize("field", NUTRITION_FIELDS)
def test_nutrition_field_accepts_string(field: str):
"""String values should be accepted as-is for every nutrition field."""
n = Nutrition(**{field: "650 kcal"})
assert getattr(n, field) == "650 kcal"
@pytest.mark.unit
@pytest.mark.parametrize("field", NUTRITION_FIELDS)
def test_nutrition_field_coerces_int(field: str):
"""Integer values should be coerced to strings for every nutrition field."""
n = Nutrition(**{field: 260})
assert getattr(n, field) == "260"
@pytest.mark.unit
@pytest.mark.parametrize("field", NUTRITION_FIELDS)
def test_nutrition_field_coerces_float(field: str):
"""Float values should be coerced to strings for every nutrition field."""
n = Nutrition(**{field: 260.5})
assert getattr(n, field) == "260.5"
@pytest.mark.unit
@pytest.mark.parametrize("field", NUTRITION_FIELDS)
def test_nutrition_field_accepts_none(field: str):
"""None should remain None for every nutrition field."""
n = Nutrition(**{field: None})
assert getattr(n, field) is None
@pytest.mark.unit
@pytest.mark.parametrize("field", NUTRITION_FIELDS)
def test_nutrition_field_rejects_bool(field: str):
"""Boolean values should not be silently coerced to strings."""
with pytest.raises(ValidationError):
Nutrition(**{field: True})
@pytest.mark.unit
def test_nutrition_mixed_types():
"""Nutrition model should handle a mix of string, int, and None values."""
n = Nutrition(
calories="650 kcal",
proteinContent=18,
fatContent=None,
)
assert n.calories == "650 kcal"
assert n.proteinContent == "18"
assert n.fatContent is None