Add unit tests for Pydantic schemas

46 tests across 12 schema classes covering CamelModel alias generation,
required field validation, optional field defaults, camelCase input/output,
nested model coercion, and from_attributes support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 12:41:22 +01:00
parent b0ac3714a9
commit 4aae12cd72
2 changed files with 317 additions and 10 deletions

View File

@@ -0,0 +1,306 @@
"""Unit tests for Pydantic schemas."""
import pytest
from pydantic import ValidationError
from app.schemas.base import CamelModel
from app.schemas.boss import BossReorderItem, BossReorderRequest, BossResultCreate
from app.schemas.encounter import EncounterCreate, EncounterUpdate
from app.schemas.game import (
GameCreate,
GameUpdate,
RouteReorderItem,
RouteReorderRequest,
)
from app.schemas.genlocke import GenlockeCreate
from app.schemas.pokemon import EvolutionCreate, PokemonCreate
from app.schemas.run import RunCreate, RunUpdate
class TestCamelModel:
def test_snake_case_field_name_accepted(self):
class M(CamelModel):
game_id: int
assert M(game_id=1).game_id == 1
def test_camel_case_alias_accepted(self):
class M(CamelModel):
game_id: int
assert M(**{"gameId": 1}).game_id == 1
def test_serializes_to_camel_case(self):
class M(CamelModel):
game_id: int
is_shiny: bool
data = M(game_id=1, is_shiny=True).model_dump(by_alias=True)
assert data == {"gameId": 1, "isShiny": True}
def test_snake_case_not_in_serialized_output(self):
class M(CamelModel):
version_group_id: int
data = M(version_group_id=5).model_dump(by_alias=True)
assert "version_group_id" not in data
assert "versionGroupId" in data
def test_from_attributes(self):
class FakeOrm:
game_id = 42
class M(CamelModel):
game_id: int
assert M.model_validate(FakeOrm()).game_id == 42
class TestRunCreate:
def test_valid_minimum(self):
run = RunCreate(game_id=1, name="Nuzlocke #1")
assert run.game_id == 1
assert run.name == "Nuzlocke #1"
assert run.rules == {}
assert run.naming_scheme is None
def test_camel_case_input(self):
run = RunCreate(**{"gameId": 5, "name": "Run"})
assert run.game_id == 5
def test_missing_game_id_raises(self):
with pytest.raises(ValidationError):
RunCreate(name="Run")
def test_missing_name_raises(self):
with pytest.raises(ValidationError):
RunCreate(game_id=1)
def test_rules_accepts_arbitrary_data(self):
run = RunCreate(game_id=1, name="x", rules={"duplicatesClause": True})
assert run.rules["duplicatesClause"] is True
def test_naming_scheme_accepted(self):
run = RunCreate(game_id=1, name="x", naming_scheme="nature")
assert run.naming_scheme == "nature"
class TestRunUpdate:
def test_all_fields_optional(self):
update = RunUpdate()
assert update.name is None
assert update.status is None
assert update.rules is None
assert update.naming_scheme is None
def test_partial_update(self):
update = RunUpdate(name="New Name")
assert update.name == "New Name"
assert update.status is None
def test_hof_encounter_ids(self):
update = RunUpdate(hof_encounter_ids=[1, 2, 3])
assert update.hof_encounter_ids == [1, 2, 3]
class TestGameCreate:
def test_valid_minimum(self):
game = GameCreate(name="Pokemon Red", slug="red", generation=1, region="Kanto")
assert game.name == "Pokemon Red"
assert game.slug == "red"
assert game.generation == 1
assert game.region == "Kanto"
def test_optional_fields_default_none(self):
game = GameCreate(name="Pokemon Red", slug="red", generation=1, region="Kanto")
assert game.category is None
assert game.box_art_url is None
assert game.release_year is None
assert game.color is None
def test_missing_required_field_raises(self):
with pytest.raises(ValidationError):
GameCreate(name="Pokemon Red", slug="red", generation=1) # missing region
def test_camel_case_input(self):
game = GameCreate(
**{
"name": "Gold",
"slug": "gold",
"generation": 2,
"region": "Johto",
"boxArtUrl": "/art.png",
}
)
assert game.box_art_url == "/art.png"
class TestGameUpdate:
def test_all_fields_optional(self):
assert GameUpdate().name is None
def test_partial_update(self):
update = GameUpdate(name="New Name", generation=3)
assert update.name == "New Name"
assert update.generation == 3
assert update.region is None
class TestEncounterCreate:
def test_valid_minimum(self):
enc = EncounterCreate(route_id=1, pokemon_id=25, status="caught")
assert enc.route_id == 1
assert enc.pokemon_id == 25
assert enc.status == "caught"
assert enc.is_shiny is False
assert enc.nickname is None
def test_camel_case_input(self):
enc = EncounterCreate(
**{"routeId": 1, "pokemonId": 25, "status": "caught", "isShiny": True}
)
assert enc.route_id == 1
assert enc.is_shiny is True
def test_missing_pokemon_id_raises(self):
with pytest.raises(ValidationError):
EncounterCreate(route_id=1, status="caught")
def test_missing_status_raises(self):
with pytest.raises(ValidationError):
EncounterCreate(route_id=1, pokemon_id=25)
def test_origin_accepted(self):
enc = EncounterCreate(route_id=1, pokemon_id=1, status="caught", origin="gift")
assert enc.origin == "gift"
class TestEncounterUpdate:
def test_all_fields_optional(self):
update = EncounterUpdate()
assert update.nickname is None
assert update.status is None
assert update.faint_level is None
assert update.death_cause is None
assert update.current_pokemon_id is None
class TestBossResultCreate:
def test_valid_minimum(self):
result = BossResultCreate(boss_battle_id=1, result="win")
assert result.boss_battle_id == 1
assert result.result == "win"
assert result.attempts == 1
def test_attempts_default_one(self):
assert BossResultCreate(boss_battle_id=1, result="loss").attempts == 1
def test_custom_attempts(self):
assert (
BossResultCreate(boss_battle_id=1, result="win", attempts=3).attempts == 3
)
def test_missing_boss_battle_id_raises(self):
with pytest.raises(ValidationError):
BossResultCreate(result="win")
class TestBossReorderRequest:
def test_nested_items_accepted(self):
req = BossReorderRequest(bosses=[BossReorderItem(id=1, order=2)])
assert req.bosses[0].id == 1
assert req.bosses[0].order == 2
def test_dict_input_coerced(self):
req = BossReorderRequest(**{"bosses": [{"id": 3, "order": 1}]})
assert req.bosses[0].id == 3
def test_empty_list_accepted(self):
assert BossReorderRequest(bosses=[]).bosses == []
class TestRouteReorderRequest:
def test_nested_items_accepted(self):
req = RouteReorderRequest(routes=[RouteReorderItem(id=10, order=1)])
assert req.routes[0].id == 10
def test_dict_input_coerced(self):
req = RouteReorderRequest(**{"routes": [{"id": 5, "order": 3}]})
assert req.routes[0].order == 3
class TestGenlockeCreate:
def test_valid_minimum(self):
gc = GenlockeCreate(name="My Genlocke", game_ids=[1, 2, 3])
assert gc.name == "My Genlocke"
assert gc.game_ids == [1, 2, 3]
assert gc.genlocke_rules == {}
assert gc.nuzlocke_rules == {}
assert gc.naming_scheme is None
def test_missing_name_raises(self):
with pytest.raises(ValidationError):
GenlockeCreate(game_ids=[1, 2])
def test_missing_game_ids_raises(self):
with pytest.raises(ValidationError):
GenlockeCreate(name="My Genlocke")
def test_camel_case_input(self):
gc = GenlockeCreate(**{"name": "x", "gameIds": [1], "namingScheme": "types"})
assert gc.naming_scheme == "types"
class TestPokemonCreate:
def test_valid_minimum(self):
p = PokemonCreate(
pokeapi_id=25, national_dex=25, name="Pikachu", types=["electric"]
)
assert p.name == "Pikachu"
assert p.types == ["electric"]
assert p.sprite_url is None
def test_multi_type(self):
p = PokemonCreate(
pokeapi_id=6, national_dex=6, name="Charizard", types=["fire", "flying"]
)
assert p.types == ["fire", "flying"]
def test_missing_required_raises(self):
with pytest.raises(ValidationError):
PokemonCreate(pokeapi_id=1, national_dex=1, name="x") # missing types
class TestEvolutionCreate:
def test_valid_minimum(self):
evo = EvolutionCreate(from_pokemon_id=1, to_pokemon_id=2, trigger="level-up")
assert evo.from_pokemon_id == 1
assert evo.to_pokemon_id == 2
assert evo.trigger == "level-up"
assert evo.min_level is None
assert evo.item is None
def test_all_optional_fields(self):
evo = EvolutionCreate(
from_pokemon_id=1,
to_pokemon_id=2,
trigger="use-item",
min_level=16,
item="fire-stone",
held_item=None,
condition="day",
region="Kanto",
)
assert evo.min_level == 16
assert evo.item == "fire-stone"
assert evo.region == "Kanto"
def test_missing_trigger_raises(self):
with pytest.raises(ValidationError):
EvolutionCreate(from_pokemon_id=1, to_pokemon_id=2)
def test_camel_case_input(self):
evo = EvolutionCreate(
**{"fromPokemonId": 1, "toPokemonId": 2, "trigger": "level-up"}
)
assert evo.from_pokemon_id == 1