feat: add decision_service with unit tests
Service layer provides validation, delegation to repository, and get_disliked_listing_ids for filtering. All 7 unit tests pass.
This commit is contained in:
parent
91c8c884d2
commit
4877a5fc9f
2 changed files with 136 additions and 0 deletions
54
services/decision_service.py
Normal file
54
services/decision_service.py
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
"""Unified decision service -- shared between CLI and HTTP API."""
|
||||||
|
from models.decision import ListingDecision
|
||||||
|
from repositories.decision_repository import DecisionRepository
|
||||||
|
|
||||||
|
VALID_DECISIONS = {"liked", "disliked"}
|
||||||
|
|
||||||
|
|
||||||
|
def set_decision(
|
||||||
|
repository: DecisionRepository,
|
||||||
|
user_id: int,
|
||||||
|
listing_id: int,
|
||||||
|
listing_type: str,
|
||||||
|
decision: str,
|
||||||
|
) -> ListingDecision:
|
||||||
|
if decision not in VALID_DECISIONS:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid decision '{decision}'. Must be one of: {VALID_DECISIONS}"
|
||||||
|
)
|
||||||
|
return repository.upsert_decision(
|
||||||
|
user_id=user_id,
|
||||||
|
listing_id=listing_id,
|
||||||
|
listing_type=listing_type,
|
||||||
|
decision=decision,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_decisions(
|
||||||
|
repository: DecisionRepository,
|
||||||
|
user_id: int,
|
||||||
|
) -> list[ListingDecision]:
|
||||||
|
return repository.get_decisions_for_user(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_decision(
|
||||||
|
repository: DecisionRepository,
|
||||||
|
user_id: int,
|
||||||
|
listing_id: int,
|
||||||
|
listing_type: str,
|
||||||
|
) -> bool:
|
||||||
|
return repository.delete_decision(
|
||||||
|
user_id=user_id,
|
||||||
|
listing_id=listing_id,
|
||||||
|
listing_type=listing_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_disliked_listing_ids(
|
||||||
|
repository: DecisionRepository,
|
||||||
|
user_id: int,
|
||||||
|
listing_type: str,
|
||||||
|
) -> set[int]:
|
||||||
|
return repository.get_disliked_listing_ids(
|
||||||
|
user_id=user_id, listing_type=listing_type
|
||||||
|
)
|
||||||
82
tests/unit/test_decision_service.py
Normal file
82
tests/unit/test_decision_service.py
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
"""Unit tests for decision_service."""
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from models.decision import ListingDecision
|
||||||
|
from services import decision_service
|
||||||
|
|
||||||
|
|
||||||
|
class TestSetDecision:
|
||||||
|
def test_set_liked(self) -> None:
|
||||||
|
repo = MagicMock()
|
||||||
|
repo.upsert_decision.return_value = ListingDecision(
|
||||||
|
id=1, user_id=1, listing_id=100, listing_type="RENT", decision="liked"
|
||||||
|
)
|
||||||
|
result = decision_service.set_decision(
|
||||||
|
repo, user_id=1, listing_id=100,
|
||||||
|
listing_type="RENT", decision="liked",
|
||||||
|
)
|
||||||
|
assert result.decision == "liked"
|
||||||
|
repo.upsert_decision.assert_called_once_with(
|
||||||
|
user_id=1, listing_id=100, listing_type="RENT", decision="liked"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_set_disliked(self) -> None:
|
||||||
|
repo = MagicMock()
|
||||||
|
repo.upsert_decision.return_value = ListingDecision(
|
||||||
|
id=1, user_id=1, listing_id=100, listing_type="RENT", decision="disliked"
|
||||||
|
)
|
||||||
|
result = decision_service.set_decision(
|
||||||
|
repo, user_id=1, listing_id=100,
|
||||||
|
listing_type="RENT", decision="disliked",
|
||||||
|
)
|
||||||
|
assert result.decision == "disliked"
|
||||||
|
|
||||||
|
def test_invalid_decision_raises(self) -> None:
|
||||||
|
repo = MagicMock()
|
||||||
|
with pytest.raises(ValueError, match="Invalid decision"):
|
||||||
|
decision_service.set_decision(
|
||||||
|
repo, user_id=1, listing_id=100,
|
||||||
|
listing_type="RENT", decision="maybe",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetDecisions:
|
||||||
|
def test_returns_all_decisions(self) -> None:
|
||||||
|
repo = MagicMock()
|
||||||
|
repo.get_decisions_for_user.return_value = [
|
||||||
|
ListingDecision(
|
||||||
|
id=1, user_id=1, listing_id=100,
|
||||||
|
listing_type="RENT", decision="liked",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
result = decision_service.get_decisions(repo, user_id=1)
|
||||||
|
assert len(result) == 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestClearDecision:
|
||||||
|
def test_clear_existing(self) -> None:
|
||||||
|
repo = MagicMock()
|
||||||
|
repo.delete_decision.return_value = True
|
||||||
|
result = decision_service.clear_decision(
|
||||||
|
repo, user_id=1, listing_id=100, listing_type="RENT"
|
||||||
|
)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
def test_clear_nonexistent(self) -> None:
|
||||||
|
repo = MagicMock()
|
||||||
|
repo.delete_decision.return_value = False
|
||||||
|
result = decision_service.clear_decision(
|
||||||
|
repo, user_id=1, listing_id=100, listing_type="RENT"
|
||||||
|
)
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetDislikedListingIds:
|
||||||
|
def test_returns_disliked_set(self) -> None:
|
||||||
|
repo = MagicMock()
|
||||||
|
repo.get_disliked_listing_ids.return_value = {200, 300}
|
||||||
|
result = decision_service.get_disliked_listing_ids(
|
||||||
|
repo, user_id=1, listing_type="RENT"
|
||||||
|
)
|
||||||
|
assert result == {200, 300}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue