bookmyservice-mhs / app /tests /test_services.py
MukeshKapoor25's picture
feat(merchant): improve favorite and recent merchants fetching logic
9f7a7b2
"""
Regression tests for service layer components
"""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from typing import Dict, Any
class TestMerchantService:
"""Test merchant service functionality"""
@patch('app.nosql.db')
@pytest.mark.asyncio
async def test_get_merchants_success(self, mock_db, sample_merchant_data):
"""Test successful merchant retrieval"""
from app.services.merchant import get_merchants
# Mock the MongoDB collection
mock_collection = AsyncMock()
mock_db.__getitem__.return_value = mock_collection
mock_collection.find.return_value.limit.return_value.skip.return_value.to_list.return_value = [sample_merchant_data]
result = await get_merchants(limit=10, skip=0)
assert len(result) == 1
assert result[0]["name"] == "Test Hair Salon"
@patch('app.nosql.db')
@pytest.mark.asyncio
async def test_get_merchant_by_id_success(self, mock_db, sample_merchant_data):
"""Test successful merchant retrieval by ID"""
from app.services.merchant import get_merchant_by_id
# Mock the MongoDB collection
mock_collection = AsyncMock()
mock_db.__getitem__.return_value = mock_collection
mock_collection.find_one.return_value = sample_merchant_data
result = await get_merchant_by_id("test_merchant_123")
assert result["_id"] == "test_merchant_123"
assert result["name"] == "Test Hair Salon"
@patch('app.nosql.db')
@pytest.mark.asyncio
async def test_get_merchant_by_id_not_found(self, mock_db):
"""Test merchant not found scenario"""
from app.services.merchant import get_merchant_by_id
# Mock the MongoDB collection
mock_collection = AsyncMock()
mock_db.__getitem__.return_value = mock_collection
mock_collection.find_one.return_value = None
result = await get_merchant_by_id("nonexistent_id")
assert result is None
@patch('app.nosql.db')
@pytest.mark.asyncio
async def test_search_merchants_with_location(self, mock_db, sample_merchant_data):
"""Test merchant search with location parameters"""
from app.services.merchant import search_merchants
# Mock the MongoDB collection
mock_collection = AsyncMock()
mock_db.__getitem__.return_value = mock_collection
mock_collection.find.return_value.limit.return_value.to_list.return_value = [sample_merchant_data]
result = await search_merchants(
latitude=40.7128,
longitude=-74.0060,
radius=5000,
category="salon"
)
assert len(result) == 1
assert result[0]["category"] == "salon"
@patch('app.nosql.redis_client')
@patch('app.nosql.db')
@pytest.mark.asyncio
async def test_merchant_caching(self, mock_db, mock_redis, sample_merchant_data):
"""Test merchant data caching"""
from app.services.merchant import get_merchants
# Mock the MongoDB collection
mock_collection = AsyncMock()
mock_db.__getitem__.return_value = mock_collection
mock_collection.find.return_value.limit.return_value.skip.return_value.to_list.return_value = [sample_merchant_data]
# Mock Redis
mock_redis.get.return_value = None # Cache miss
mock_redis.set.return_value = True
result = await get_merchants(limit=10, skip=0)
assert len(result) == 1
assert result[0]["name"] == "Test Hair Salon"
@patch('app.services.merchant.execute_query')
@pytest.mark.asyncio
async def test_fetch_favorite_merchants_success(self, mock_execute_query):
"""Test successful fetch of favorite merchants"""
from app.services.merchant import fetch_favorite_merchants
# Create merchant data that matches our projection fields
merchant_data = {
"merchant_id": "test_merchant_123",
"business_name": "Test Hair Salon",
"business_url": "test-hair-salon",
"description": "A great hair salon",
"city": "Chennai",
"merchant_category": "salon",
"merchant_subcategory": "hair_salon",
"average_rating": 4.5,
"location_id": "IN-SOUTH"
}
# Mock the favorites query result
mock_execute_query.return_value = [merchant_data]
result = await fetch_favorite_merchants("customer_123", "IN-SOUTH", limit=5)
print(f"Result: {result}")
print(f"Type of result: {type(result)}")
if isinstance(result, dict):
print(f"Keys in result: {result.keys()}")
if "favorite_merchants" in result:
print(f"Favorite merchants count: {len(result['favorite_merchants'])}")
print(f"Favorite merchants: {result['favorite_merchants']}")
assert "favorite_merchants" in result
assert "recent_merchants" in result
assert len(result["favorite_merchants"]) == 1
assert len(result["recent_merchants"]) == 0 # Currently returns empty
assert result["favorite_merchants"][0]["business_name"] == "Test Hair Salon"
assert result["favorite_merchants"][0]["merchant_id"] == "test_merchant_123"
# Verify the query was called with correct collection
mock_execute_query.assert_called_once()
args, kwargs = mock_execute_query.call_args
assert args[0] == "favourites" # Should query favourites collection
@patch('app.repositories.db_repository.execute_query')
@pytest.mark.asyncio
async def test_fetch_favorite_merchants_empty_result(self, mock_execute_query):
"""Test fetch favorite merchants with no favorites"""
from app.services.merchant import fetch_favorite_merchants
# Mock empty result
mock_execute_query.return_value = []
result = await fetch_favorite_merchants("customer_456", "IN-SOUTH", limit=5)
assert result["favorite_merchants"] == []
assert result["recent_merchants"] == []
@patch('app.repositories.db_repository.execute_query')
@pytest.mark.asyncio
async def test_fetch_favorite_merchants_error_handling(self, mock_execute_query):
"""Test error handling in fetch favorite merchants"""
from app.services.merchant import fetch_favorite_merchants
# Mock database error
mock_execute_query.side_effect = Exception("Database connection error")
result = await fetch_favorite_merchants("customer_789", "IN-SOUTH", limit=5)
# Should return empty lists on error
assert result["favorite_merchants"] == []
assert result["recent_merchants"] == []
class TestHelperService:
"""Test helper service functionality"""
@pytest.mark.asyncio
async def test_process_free_text_basic(self):
"""Test basic free text processing"""
from app.services.helper import process_free_text
result = await process_free_text("find a hair salon", 40.7128, -74.0060)
assert "query" in result
assert "extracted_keywords" in result
assert result["query"] == "find a hair salon"
@pytest.mark.asyncio
async def test_process_free_text_with_location(self):
"""Test free text processing with location"""
from app.services.helper import process_free_text
result = await process_free_text("salon near me", 40.7128, -74.0060)
assert "location" in result or "search_parameters" in result
assert result["query"] == "salon near me"
@pytest.mark.asyncio
async def test_process_free_text_empty_input(self):
"""Test free text processing with empty input"""
from app.services.helper import process_free_text
result = await process_free_text("", 40.7128, -74.0060)
# Should handle empty input gracefully
assert "query" in result
assert result["query"] == ""
@pytest.mark.asyncio
async def test_extract_keywords(self):
"""Test keyword extraction functionality"""
from app.services.helper import extract_keywords
keywords = extract_keywords("find the best hair salon with parking")
assert isinstance(keywords, list)
assert len(keywords) > 0
# Should extract relevant keywords
assert any("hair" in kw.lower() or "salon" in kw.lower() for kw in keywords)
@pytest.mark.asyncio
async def test_suggest_category(self):
"""Test category suggestion functionality"""
from app.services.helper import suggest_category
category = suggest_category("hair salon")
assert category is not None
assert category in ["salon", "beauty", "hair_salon"]
class TestSearchHelpers:
"""Test search helper functionality"""
@pytest.mark.asyncio
async def test_build_search_query(self):
"""Test search query building"""
from app.services.search_helpers import build_search_query
params = {
"category": "salon",
"latitude": 40.7128,
"longitude": -74.0060,
"radius": 5000
}
query = build_search_query(params)
assert isinstance(query, dict)
assert "location" in query or "category" in query
@pytest.mark.asyncio
async def test_apply_filters(self):
"""Test filter application"""
from app.services.search_helpers import apply_filters
merchants = [
{"name": "Salon A", "average_rating": 4.5, "price_range": "$$"},
{"name": "Salon B", "average_rating": 3.5, "price_range": "$"},
{"name": "Salon C", "average_rating": 4.8, "price_range": "$$$"}
]
filters = {"min_rating": 4.0, "max_price_range": "$$"}
filtered = apply_filters(merchants, filters)
assert len(filtered) <= len(merchants)
# Should only include merchants meeting criteria
for merchant in filtered:
assert merchant["average_rating"] >= 4.0
@pytest.mark.asyncio
async def test_sort_results(self):
"""Test result sorting"""
from app.services.search_helpers import sort_results
merchants = [
{"name": "Salon A", "average_rating": 4.5, "distance": 1000},
{"name": "Salon B", "average_rating": 4.8, "distance": 2000},
{"name": "Salon C", "average_rating": 4.2, "distance": 500}
]
# Sort by rating
sorted_by_rating = sort_results(merchants, "rating")
assert sorted_by_rating[0]["average_rating"] >= sorted_by_rating[1]["average_rating"]
# Sort by distance
sorted_by_distance = sort_results(merchants, "distance")
assert sorted_by_distance[0]["distance"] <= sorted_by_distance[1]["distance"]
class TestAdvancedNLPService:
"""Test advanced NLP service functionality"""
@pytest.mark.asyncio
async def test_nlp_pipeline_initialization(self):
"""Test NLP pipeline initialization"""
from app.services.advanced_nlp import AdvancedNLPPipeline
pipeline = AdvancedNLPPipeline()
assert pipeline.intent_classifier is not None
assert pipeline.entity_extractor is not None
assert pipeline.semantic_matcher is not None
assert pipeline.context_processor is not None
@pytest.mark.asyncio
async def test_intent_classification(self):
"""Test intent classification"""
from app.services.advanced_nlp import IntentClassifier
classifier = IntentClassifier()
# Test search intent
intent, confidence = classifier.get_primary_intent("find a hair salon")
assert intent == "SEARCH_SERVICE"
assert confidence > 0.0
# Test quality filter intent
intent, confidence = classifier.get_primary_intent("best salon in town")
assert intent == "FILTER_QUALITY"
assert confidence > 0.0
@pytest.mark.asyncio
async def test_entity_extraction(self):
"""Test entity extraction"""
from app.services.advanced_nlp import BusinessEntityExtractor
extractor = BusinessEntityExtractor()
entities = extractor.extract_entities("hair salon with parking near me")
assert isinstance(entities, dict)
assert "service_types" in entities or "amenities" in entities
@pytest.mark.asyncio
async def test_semantic_matching(self):
"""Test semantic matching"""
from app.services.advanced_nlp import SemanticMatcher
matcher = SemanticMatcher()
matches = matcher.find_similar_services("hair salon")
assert isinstance(matches, list)
assert len(matches) > 0
# Should return tuples of (service, similarity_score)
for match in matches:
assert len(match) == 2
assert isinstance(match[1], float)
assert 0.0 <= match[1] <= 1.0
@pytest.mark.asyncio
async def test_context_processing(self):
"""Test context-aware processing"""
from app.services.advanced_nlp import ContextAwareProcessor
processor = ContextAwareProcessor()
result = await processor.process_with_context(
"spa treatment",
{"service_categories": ["spa"]},
[("spa", 0.9)]
)
assert isinstance(result, dict)
@pytest.mark.asyncio
async def test_complete_nlp_pipeline(self):
"""Test complete NLP pipeline processing"""
from app.services.advanced_nlp import AdvancedNLPPipeline
pipeline = AdvancedNLPPipeline()
result = await pipeline.process_query("find the best hair salon near me")
assert "query" in result
assert "primary_intent" in result
assert "entities" in result
assert "similar_services" in result
assert "search_parameters" in result
assert "processing_time" in result
@pytest.mark.asyncio
async def test_nlp_caching(self):
"""Test NLP result caching"""
from app.services.advanced_nlp import AsyncNLPProcessor
processor = AsyncNLPProcessor()
def dummy_processor(text):
return {"processed": text.upper()}
# First call
result1 = await processor.process_async("test", dummy_processor)
# Second call should use cache
result2 = await processor.process_async("test", dummy_processor)
assert result1 == result2
@pytest.mark.asyncio
async def test_nlp_error_handling(self):
"""Test NLP error handling"""
from app.services.advanced_nlp import AdvancedNLPPipeline
pipeline = AdvancedNLPPipeline()
# Test with empty query
result = await pipeline.process_query("")
assert "query" in result
assert result["query"] == ""
# Should handle gracefully without crashing
class TestServiceIntegration:
"""Test service integration scenarios"""
@patch('app.services.merchant.search_merchants')
@patch('app.services.advanced_nlp.advanced_nlp_pipeline')
@pytest.mark.asyncio
async def test_nlp_to_merchant_search_integration(self, mock_nlp, mock_search, sample_merchant_data):
"""Test integration between NLP processing and merchant search"""
# Mock NLP pipeline response
mock_nlp.process_query.return_value = {
"search_parameters": {
"merchant_category": "salon",
"radius": 5000,
"amenities": ["parking"]
}
}
# Mock merchant search response
mock_search.return_value = [sample_merchant_data]
# Simulate the integration
nlp_result = await mock_nlp.process_query("salon with parking")
search_params = nlp_result["search_parameters"]
merchants = await mock_search(**search_params)
assert len(merchants) == 1
assert merchants[0]["category"] == "salon"
@patch('app.repositories.cache_repository.cache_search_results')
@patch('app.services.merchant.search_merchants')
@pytest.mark.asyncio
async def test_search_result_caching(self, mock_search, mock_cache, sample_merchant_data):
"""Test search result caching integration"""
mock_search.return_value = [sample_merchant_data]
# Perform search
results = await mock_search(category="salon")
# Cache should be called
mock_cache.assert_called_once()
assert len(results) == 1
@pytest.mark.asyncio
async def test_error_propagation(self):
"""Test error propagation between services"""
from app.services.merchant import get_merchant_by_id
with patch('app.nosql.db') as mock_db:
mock_collection = AsyncMock()
mock_db.__getitem__.return_value = mock_collection
mock_collection.find_one.side_effect = Exception("Database connection failed")
with pytest.raises(Exception) as exc_info:
await get_merchant_by_id("test_id")
assert "Database connection failed" in str(exc_info.value)
class TestServicePerformance:
"""Test service performance characteristics"""
@pytest.mark.asyncio
async def test_concurrent_merchant_requests(self, sample_merchant_data):
"""Test concurrent merchant service requests"""
import asyncio
from app.services.merchant import get_merchant_by_id
with patch('app.nosql.db') as mock_db:
mock_collection = AsyncMock()
mock_db.__getitem__.return_value = mock_collection
mock_collection.find_one.return_value = sample_merchant_data
# Create multiple concurrent requests
tasks = [
get_merchant_by_id(f"merchant_{i}")
for i in range(10)
]
results = await asyncio.gather(*tasks)
assert len(results) == 10
assert all(result["name"] == "Test Hair Salon" for result in results)
@pytest.mark.asyncio
async def test_nlp_processing_performance(self):
"""Test NLP processing performance"""
import time
from app.services.advanced_nlp import AdvancedNLPPipeline
pipeline = AdvancedNLPPipeline()
start_time = time.time()
result = await pipeline.process_query("find a hair salon")
processing_time = time.time() - start_time
# Should process within reasonable time
assert processing_time < 2.0 # 2 seconds max
assert "processing_time" in result
assert result["processing_time"] > 0