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>
307 lines
9.6 KiB
Python
307 lines
9.6 KiB
Python
"""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
|