From 4877a5fc9fe4588aabca9844ed883e3f065bec6a Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sat, 21 Feb 2026 13:52:54 +0000 Subject: [PATCH] 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. --- services/decision_service.py | 54 +++++++++++++++++++ tests/unit/test_decision_service.py | 82 +++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 services/decision_service.py create mode 100644 tests/unit/test_decision_service.py diff --git a/services/decision_service.py b/services/decision_service.py new file mode 100644 index 0000000..4faae8a --- /dev/null +++ b/services/decision_service.py @@ -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 + ) diff --git a/tests/unit/test_decision_service.py b/tests/unit/test_decision_service.py new file mode 100644 index 0000000..c7a48a5 --- /dev/null +++ b/tests/unit/test_decision_service.py @@ -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}