import pytest from finbot.categories import CATEGORIES, categorise, coerce, is_valid, label class TestCategorise: @pytest.mark.parametrize( "text,expected", [ ("uber ride home", "transport"), ("netflix monthly", "subscriptions"), ("bigbasket order", "groceries"), ("swiggy dinner", "food_dining"), ("petrol pump", "fuel"), ("electricity bill", "utilities"), ("apollo pharmacy", "health"), ("bookmyshow tickets", "entertainment"), ("amazon order", "shopping"), ("atm withdrawal", "cash"), ("salary credited", "income"), ], ) def test_keyword_hits(self, text, expected): category, confidence = categorise(text) assert category == expected assert confidence == 1.0 def test_unknown_returns_other_with_zero_confidence(self): category, confidence = categorise("blorptastic widget") assert category == "other" assert confidence == 0.0 def test_empty_input(self): assert categorise("") == ("other", 0.0) def test_specificity_order_fuel_beats_transport(self): # A petrol pump is fuel, even though it is transport-adjacent. assert categorise("indian oil petrol")[0] == "fuel" def test_specificity_order_subscription_beats_entertainment(self): assert categorise("netflix")[0] == "subscriptions" def test_word_boundaries_prevent_false_hits(self): # "cashew" must not match the "cash" keyword. assert categorise("cashew nuts")[0] != "cash" # "gasket" must not match "gas station". assert categorise("engine gasket")[0] != "fuel" def test_merchant_is_considered(self): category, _ = categorise("monthly charge", merchant="Spotify") assert category == "subscriptions" def test_case_insensitive(self): assert categorise("UBER RIDE")[0] == "transport" class TestCoerce: @pytest.mark.parametrize( "raw,expected", [ ("food_dining", "food_dining"), ("Food", "food_dining"), ("dining", "food_dining"), ("grocery", "groceries"), ("healthcare", "health"), ("Subscription", "subscriptions"), ("misc", "other"), ("", "other"), ("total nonsense", "other"), ], ) def test_coerce(self, raw, expected): assert coerce(raw) == expected def test_every_result_is_a_valid_category(self): # A stray label from a model must never reach the ledger, or totals # fragment across categories that do not exist. for raw in ["", "???", "Investments", "food", "TRANSPORT", "x" * 200]: assert is_valid(coerce(raw)) class TestLabels: def test_every_category_has_a_label(self): for category in CATEGORIES: assert label(category) assert label(category) != category or "_" not in category