feat: add Promote Animal action route (Step 9.1 continued)
- Add /actions/promote/{animal_id} GET and /actions/animal-promote POST routes
- Add promote_form() template with nickname, sex, repro_status fields
- Add AnimalRepository.get() method for single-animal lookup
- 10 new tests for promote form rendering and submission
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -423,3 +423,164 @@ class TestHatchRecordingValidation:
|
||||
)
|
||||
|
||||
assert resp.status_code == 422
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Animal Promotion Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def unpromoted_animal_id(seeded_db, client, location_strip1_id):
|
||||
"""Create an unpromoted animal for testing."""
|
||||
# Create a cohort with one animal
|
||||
resp = client.post(
|
||||
"/actions/animal-cohort",
|
||||
data={
|
||||
"species": "duck",
|
||||
"location_id": location_strip1_id,
|
||||
"count": "1",
|
||||
"life_stage": "adult",
|
||||
"sex": "female",
|
||||
"origin": "purchased",
|
||||
"nonce": "test-fixture-cohort-1",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
# Get the animal ID
|
||||
row = seeded_db.execute(
|
||||
"SELECT animal_id FROM animal_registry WHERE identified = 0 ORDER BY animal_id DESC LIMIT 1"
|
||||
).fetchone()
|
||||
return row[0]
|
||||
|
||||
|
||||
class TestPromoteFormRendering:
|
||||
"""Tests for GET /actions/promote/{animal_id} form rendering."""
|
||||
|
||||
def test_promote_form_renders(self, client, unpromoted_animal_id):
|
||||
"""GET /actions/promote/{animal_id} returns 200 with form elements."""
|
||||
resp = client.get(f"/actions/promote/{unpromoted_animal_id}")
|
||||
assert resp.status_code == 200
|
||||
assert "Promote" in resp.text
|
||||
|
||||
def test_promote_form_shows_animal_info(self, client, unpromoted_animal_id):
|
||||
"""Form shows animal information."""
|
||||
resp = client.get(f"/actions/promote/{unpromoted_animal_id}")
|
||||
assert resp.status_code == 200
|
||||
# Should show animal ID (truncated)
|
||||
assert unpromoted_animal_id[:8] in resp.text
|
||||
|
||||
def test_promote_form_has_nickname_field(self, client, unpromoted_animal_id):
|
||||
"""Form has nickname input field."""
|
||||
resp = client.get(f"/actions/promote/{unpromoted_animal_id}")
|
||||
assert resp.status_code == 200
|
||||
assert 'name="nickname"' in resp.text
|
||||
|
||||
def test_promote_form_has_sex_dropdown(self, client, unpromoted_animal_id):
|
||||
"""Form has sex dropdown."""
|
||||
resp = client.get(f"/actions/promote/{unpromoted_animal_id}")
|
||||
assert resp.status_code == 200
|
||||
assert 'name="sex"' in resp.text
|
||||
|
||||
def test_promote_form_not_found(self, client):
|
||||
"""Non-existent animal returns 404."""
|
||||
resp = client.get("/actions/promote/01ABCDEFGHIJKLMNOPQRSTUV00")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
class TestPromoteSuccess:
|
||||
"""Tests for successful POST /actions/animal-promote."""
|
||||
|
||||
def test_promote_creates_event(self, client, seeded_db, unpromoted_animal_id):
|
||||
"""POST creates AnimalPromoted event when valid."""
|
||||
resp = client.post(
|
||||
"/actions/animal-promote",
|
||||
data={
|
||||
"animal_id": unpromoted_animal_id,
|
||||
"nickname": "Daffy",
|
||||
"nonce": "test-promote-nonce-1",
|
||||
},
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
# Should redirect
|
||||
assert resp.status_code == 303
|
||||
|
||||
# Verify event was created
|
||||
event_row = seeded_db.execute(
|
||||
"SELECT type FROM events WHERE type = 'AnimalPromoted' ORDER BY id DESC LIMIT 1"
|
||||
).fetchone()
|
||||
assert event_row is not None
|
||||
assert event_row[0] == "AnimalPromoted"
|
||||
|
||||
def test_promote_sets_identified(self, client, seeded_db, unpromoted_animal_id):
|
||||
"""POST sets identified flag on animal."""
|
||||
resp = client.post(
|
||||
"/actions/animal-promote",
|
||||
data={
|
||||
"animal_id": unpromoted_animal_id,
|
||||
"nickname": "Huey",
|
||||
"nonce": "test-promote-nonce-2",
|
||||
},
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
assert resp.status_code == 303
|
||||
|
||||
# Verify identified is now true
|
||||
row = seeded_db.execute(
|
||||
"SELECT identified FROM animal_registry WHERE animal_id = ?",
|
||||
(unpromoted_animal_id,),
|
||||
).fetchone()
|
||||
assert row[0] == 1 # identified = true
|
||||
|
||||
def test_promote_sets_nickname(self, client, seeded_db, unpromoted_animal_id):
|
||||
"""POST sets nickname on animal."""
|
||||
resp = client.post(
|
||||
"/actions/animal-promote",
|
||||
data={
|
||||
"animal_id": unpromoted_animal_id,
|
||||
"nickname": "Dewey",
|
||||
"nonce": "test-promote-nonce-3",
|
||||
},
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
assert resp.status_code == 303
|
||||
|
||||
# Verify nickname is set
|
||||
row = seeded_db.execute(
|
||||
"SELECT nickname FROM animal_registry WHERE animal_id = ?",
|
||||
(unpromoted_animal_id,),
|
||||
).fetchone()
|
||||
assert row[0] == "Dewey"
|
||||
|
||||
|
||||
class TestPromoteValidation:
|
||||
"""Tests for validation errors in POST /actions/animal-promote."""
|
||||
|
||||
def test_promote_missing_animal_id_returns_422(self, client):
|
||||
"""Missing animal_id returns 422."""
|
||||
resp = client.post(
|
||||
"/actions/animal-promote",
|
||||
data={
|
||||
"nickname": "Test",
|
||||
"nonce": "test-promote-nonce-4",
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 422
|
||||
|
||||
def test_promote_not_found_returns_404(self, client):
|
||||
"""Non-existent animal returns 404."""
|
||||
resp = client.post(
|
||||
"/actions/animal-promote",
|
||||
data={
|
||||
"animal_id": "01ABCDEFGHIJKLMNOPQRSTUV00",
|
||||
"nickname": "Test",
|
||||
"nonce": "test-promote-nonce-5",
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 404
|
||||
|
||||
Reference in New Issue
Block a user