test updates to take into account exception management
This commit is contained in:
@@ -1,15 +1,24 @@
|
||||
# tests/test_shop_service.py (simplified with fixtures)
|
||||
# tests/test_shop_service.py (updated to use custom exceptions)
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.services.shop_service import ShopService
|
||||
from app.exceptions import (
|
||||
ShopNotFoundException,
|
||||
ShopAlreadyExistsException,
|
||||
UnauthorizedShopAccessException,
|
||||
InvalidShopDataException,
|
||||
ProductNotFoundException,
|
||||
ShopProductAlreadyExistsException,
|
||||
MaxShopsReachedException,
|
||||
ValidationException,
|
||||
)
|
||||
from models.schemas.shop import ShopCreate, ShopProductCreate
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.shops
|
||||
class TestShopService:
|
||||
"""Test suite for ShopService following the application's testing patterns"""
|
||||
"""Test suite for ShopService following the application's exception patterns"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup method following the same pattern as admin service tests"""
|
||||
@@ -44,11 +53,69 @@ class TestShopService:
|
||||
shop_code=test_shop.shop_code, shop_name=test_shop.shop_name
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
with pytest.raises(ShopAlreadyExistsException) as exc_info:
|
||||
self.service.create_shop(db, shop_data, test_user)
|
||||
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "Shop code already exists" in str(exc_info.value.detail)
|
||||
exception = exc_info.value
|
||||
assert exception.status_code == 409
|
||||
assert exception.error_code == "SHOP_ALREADY_EXISTS"
|
||||
assert test_shop.shop_code.upper() in exception.message
|
||||
assert "shop_code" in exception.details
|
||||
|
||||
def test_create_shop_invalid_data_empty_code(self, db, test_user):
|
||||
"""Test shop creation fails with empty shop code"""
|
||||
shop_data = ShopCreate(shop_code="", shop_name="Test Shop")
|
||||
|
||||
with pytest.raises(InvalidShopDataException) as exc_info:
|
||||
self.service.create_shop(db, shop_data, test_user)
|
||||
|
||||
exception = exc_info.value
|
||||
assert exception.status_code == 422
|
||||
assert exception.error_code == "INVALID_SHOP_DATA"
|
||||
assert exception.details["field"] == "shop_code"
|
||||
|
||||
def test_create_shop_invalid_data_empty_name(self, db, test_user):
|
||||
"""Test shop creation fails with empty shop name"""
|
||||
shop_data = ShopCreate(shop_code="VALIDCODE", shop_name="")
|
||||
|
||||
with pytest.raises(InvalidShopDataException) as exc_info:
|
||||
self.service.create_shop(db, shop_data, test_user)
|
||||
|
||||
exception = exc_info.value
|
||||
assert exception.error_code == "INVALID_SHOP_DATA"
|
||||
assert exception.details["field"] == "shop_name"
|
||||
|
||||
def test_create_shop_invalid_code_format(self, db, test_user):
|
||||
"""Test shop creation fails with invalid shop code format"""
|
||||
shop_data = ShopCreate(shop_code="INVALID@CODE!", shop_name="Test Shop")
|
||||
|
||||
with pytest.raises(InvalidShopDataException) as exc_info:
|
||||
self.service.create_shop(db, shop_data, test_user)
|
||||
|
||||
exception = exc_info.value
|
||||
assert exception.error_code == "INVALID_SHOP_DATA"
|
||||
assert exception.details["field"] == "shop_code"
|
||||
assert "letters, numbers, underscores, and hyphens" in exception.message
|
||||
|
||||
def test_create_shop_max_shops_reached(self, db, test_user, monkeypatch):
|
||||
"""Test shop creation fails when user reaches maximum shops"""
|
||||
|
||||
# Mock the shop count check to simulate user at limit
|
||||
def mock_check_shop_limit(self, db, user):
|
||||
raise MaxShopsReachedException(max_shops=5, user_id=user.id)
|
||||
|
||||
monkeypatch.setattr(ShopService, "_check_shop_limit", mock_check_shop_limit)
|
||||
|
||||
shop_data = ShopCreate(shop_code="NEWSHOP", shop_name="New Shop")
|
||||
|
||||
with pytest.raises(MaxShopsReachedException) as exc_info:
|
||||
self.service.create_shop(db, shop_data, test_user)
|
||||
|
||||
exception = exc_info.value
|
||||
assert exception.status_code == 400
|
||||
assert exception.error_code == "MAX_SHOPS_REACHED"
|
||||
assert exception.details["max_shops"] == 5
|
||||
assert exception.details["user_id"] == test_user.id
|
||||
|
||||
def test_get_shops_regular_user(self, db, test_user, test_shop, inactive_shop):
|
||||
"""Test regular user can only see active verified shops and own shops"""
|
||||
@@ -59,7 +126,7 @@ class TestShopService:
|
||||
assert inactive_shop.shop_code not in shop_codes
|
||||
|
||||
def test_get_shops_admin_user(
|
||||
self, db, test_admin, test_shop, inactive_shop, verified_shop
|
||||
self, db, test_admin, test_shop, inactive_shop, verified_shop
|
||||
):
|
||||
"""Test admin user can see all shops with filters"""
|
||||
shops, total = self.service.get_shops(
|
||||
@@ -88,18 +155,26 @@ class TestShopService:
|
||||
assert shop.id == test_shop.id
|
||||
|
||||
def test_get_shop_by_code_not_found(self, db, test_user):
|
||||
"""Test shop not found returns appropriate error"""
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
"""Test shop not found raises proper exception"""
|
||||
with pytest.raises(ShopNotFoundException) as exc_info:
|
||||
self.service.get_shop_by_code(db, "NONEXISTENT", test_user)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
exception = exc_info.value
|
||||
assert exception.status_code == 404
|
||||
assert exception.error_code == "SHOP_NOT_FOUND"
|
||||
assert exception.details["resource_type"] == "Shop"
|
||||
assert exception.details["identifier"] == "NONEXISTENT"
|
||||
|
||||
def test_get_shop_by_code_access_denied(self, db, test_user, inactive_shop):
|
||||
"""Test regular user cannot access unverified shop they don't own"""
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
with pytest.raises(UnauthorizedShopAccessException) as exc_info:
|
||||
self.service.get_shop_by_code(db, inactive_shop.shop_code, test_user)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
exception = exc_info.value
|
||||
assert exception.status_code == 403
|
||||
assert exception.error_code == "UNAUTHORIZED_SHOP_ACCESS"
|
||||
assert exception.details["shop_code"] == inactive_shop.shop_code
|
||||
assert exception.details["user_id"] == test_user.id
|
||||
|
||||
def test_add_product_to_shop_success(self, db, test_shop, unique_product):
|
||||
"""Test successfully adding product to shop"""
|
||||
@@ -122,10 +197,14 @@ class TestShopService:
|
||||
"""Test adding non-existent product to shop fails"""
|
||||
shop_product_data = ShopProductCreate(product_id="NONEXISTENT", price="15.99")
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
with pytest.raises(ProductNotFoundException) as exc_info:
|
||||
self.service.add_product_to_shop(db, test_shop, shop_product_data)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
exception = exc_info.value
|
||||
assert exception.status_code == 404
|
||||
assert exception.error_code == "PRODUCT_NOT_FOUND"
|
||||
assert exception.details["resource_type"] == "Product"
|
||||
assert exception.details["identifier"] == "NONEXISTENT"
|
||||
|
||||
def test_add_product_to_shop_already_exists(self, db, test_shop, shop_product):
|
||||
"""Test adding product that's already in shop fails"""
|
||||
@@ -133,13 +212,17 @@ class TestShopService:
|
||||
product_id=shop_product.product.product_id, price="15.99"
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
with pytest.raises(ShopProductAlreadyExistsException) as exc_info:
|
||||
self.service.add_product_to_shop(db, test_shop, shop_product_data)
|
||||
|
||||
assert exc_info.value.status_code == 400
|
||||
exception = exc_info.value
|
||||
assert exception.status_code == 409
|
||||
assert exception.error_code == "SHOP_PRODUCT_ALREADY_EXISTS"
|
||||
assert exception.details["shop_code"] == test_shop.shop_code
|
||||
assert exception.details["product_id"] == shop_product.product.product_id
|
||||
|
||||
def test_get_shop_products_owner_access(
|
||||
self, db, test_user, test_shop, shop_product
|
||||
self, db, test_user, test_shop, shop_product
|
||||
):
|
||||
"""Test shop owner can get shop products"""
|
||||
products, total = self.service.get_shop_products(db, test_shop, test_user)
|
||||
@@ -151,93 +234,132 @@ class TestShopService:
|
||||
|
||||
def test_get_shop_products_access_denied(self, db, test_user, inactive_shop):
|
||||
"""Test non-owner cannot access unverified shop products"""
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
with pytest.raises(UnauthorizedShopAccessException) as exc_info:
|
||||
self.service.get_shop_products(db, inactive_shop, test_user)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
exception = exc_info.value
|
||||
assert exception.status_code == 403
|
||||
assert exception.error_code == "UNAUTHORIZED_SHOP_ACCESS"
|
||||
assert exception.details["shop_code"] == inactive_shop.shop_code
|
||||
assert exception.details["user_id"] == test_user.id
|
||||
|
||||
def test_get_shop_by_id(self, db, test_shop):
|
||||
"""Test getting shop by ID"""
|
||||
shop = self.service.get_shop_by_id(db, test_shop.id)
|
||||
def test_get_shop_products_with_filters(self, db, test_user, test_shop, shop_product):
|
||||
"""Test getting shop products with various filters"""
|
||||
# Test active only filter
|
||||
products, total = self.service.get_shop_products(
|
||||
db, test_shop, test_user, active_only=True
|
||||
)
|
||||
assert all(p.is_active for p in products)
|
||||
|
||||
assert shop is not None
|
||||
assert shop.id == test_shop.id
|
||||
# Test featured only filter
|
||||
products, total = self.service.get_shop_products(
|
||||
db, test_shop, test_user, featured_only=True
|
||||
)
|
||||
assert all(p.is_featured for p in products)
|
||||
|
||||
def test_get_shop_by_id_not_found(self, db):
|
||||
"""Test getting shop by ID when shop doesn't exist"""
|
||||
shop = self.service.get_shop_by_id(db, 99999)
|
||||
# Test exception handling for generic errors
|
||||
def test_create_shop_database_error(self, db, test_user, monkeypatch):
|
||||
"""Test shop creation handles database errors gracefully"""
|
||||
|
||||
assert shop is None
|
||||
def mock_commit():
|
||||
raise Exception("Database connection failed")
|
||||
|
||||
def test_shop_code_exists_true(self, db, test_shop):
|
||||
"""Test shop_code_exists returns True when shop code exists"""
|
||||
exists = self.service.shop_code_exists(db, test_shop.shop_code)
|
||||
monkeypatch.setattr(db, "commit", mock_commit)
|
||||
|
||||
assert exists is True
|
||||
shop_data = ShopCreate(shop_code="NEWSHOP", shop_name="Test Shop")
|
||||
|
||||
def test_shop_code_exists_false(self, db):
|
||||
"""Test shop_code_exists returns False when shop code doesn't exist"""
|
||||
exists = self.service.shop_code_exists(db, "NONEXISTENT")
|
||||
with pytest.raises(ValidationException) as exc_info:
|
||||
self.service.create_shop(db, shop_data, test_user)
|
||||
|
||||
assert exists is False
|
||||
exception = exc_info.value
|
||||
assert exception.status_code == 422
|
||||
assert exception.error_code == "VALIDATION_ERROR"
|
||||
assert "Failed to create shop" in exception.message
|
||||
|
||||
def test_get_product_by_id(self, db, unique_product):
|
||||
"""Test getting product by product_id"""
|
||||
product = self.service.get_product_by_id(db, unique_product.product_id)
|
||||
def test_get_shops_database_error(self, db, test_user, monkeypatch):
|
||||
"""Test get shops handles database errors gracefully"""
|
||||
|
||||
assert product is not None
|
||||
assert product.id == unique_product.id
|
||||
def mock_query(*args):
|
||||
raise Exception("Database query failed")
|
||||
|
||||
def test_get_product_by_id_not_found(self, db):
|
||||
"""Test getting product by product_id when product doesn't exist"""
|
||||
product = self.service.get_product_by_id(db, "NONEXISTENT")
|
||||
monkeypatch.setattr(db, "query", mock_query)
|
||||
|
||||
assert product is None
|
||||
with pytest.raises(ValidationException) as exc_info:
|
||||
self.service.get_shops(db, test_user)
|
||||
|
||||
def test_product_in_shop_true(self, db, test_shop, shop_product):
|
||||
"""Test product_in_shop returns True when product is in shop"""
|
||||
exists = self.service.product_in_shop(db, test_shop.id, shop_product.product_id)
|
||||
exception = exc_info.value
|
||||
assert exception.error_code == "VALIDATION_ERROR"
|
||||
assert "Failed to retrieve shops" in exception.message
|
||||
|
||||
assert exists is True
|
||||
def test_add_product_database_error(self, db, test_shop, unique_product, monkeypatch):
|
||||
"""Test add product handles database errors gracefully"""
|
||||
|
||||
def test_product_in_shop_false(self, db, test_shop, unique_product):
|
||||
"""Test product_in_shop returns False when product is not in shop"""
|
||||
exists = self.service.product_in_shop(db, test_shop.id, unique_product.id)
|
||||
def mock_commit():
|
||||
raise Exception("Database commit failed")
|
||||
|
||||
assert exists is False
|
||||
monkeypatch.setattr(db, "commit", mock_commit)
|
||||
|
||||
def test_is_shop_owner_true(self, test_shop, test_user):
|
||||
"""Test is_shop_owner returns True for shop owner"""
|
||||
is_owner = self.service.is_shop_owner(test_shop, test_user)
|
||||
shop_product_data = ShopProductCreate(
|
||||
product_id=unique_product.product_id, price="15.99"
|
||||
)
|
||||
|
||||
assert is_owner is True
|
||||
with pytest.raises(ValidationException) as exc_info:
|
||||
self.service.add_product_to_shop(db, test_shop, shop_product_data)
|
||||
|
||||
def test_is_shop_owner_false(self, inactive_shop, test_user):
|
||||
"""Test is_shop_owner returns False for non-owner"""
|
||||
is_owner = self.service.is_shop_owner(inactive_shop, test_user)
|
||||
exception = exc_info.value
|
||||
assert exception.error_code == "VALIDATION_ERROR"
|
||||
assert "Failed to add product to shop" in exception.message
|
||||
|
||||
assert is_owner is False
|
||||
|
||||
def test_can_view_shop_owner(self, test_shop, test_user):
|
||||
"""Test can_view_shop returns True for shop owner"""
|
||||
can_view = self.service.can_view_shop(test_shop, test_user)
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.shops
|
||||
class TestShopServiceExceptionDetails:
|
||||
"""Additional tests focusing specifically on exception structure and details"""
|
||||
|
||||
assert can_view is True
|
||||
def setup_method(self):
|
||||
self.service = ShopService()
|
||||
|
||||
def test_can_view_shop_admin(self, test_shop, test_admin):
|
||||
"""Test can_view_shop returns True for admin"""
|
||||
can_view = self.service.can_view_shop(test_shop, test_admin)
|
||||
def test_exception_to_dict_structure(self, db, test_user, test_shop):
|
||||
"""Test that exceptions can be properly serialized to dict for API responses"""
|
||||
shop_data = ShopCreate(
|
||||
shop_code=test_shop.shop_code, shop_name="Duplicate"
|
||||
)
|
||||
|
||||
assert can_view is True
|
||||
with pytest.raises(ShopAlreadyExistsException) as exc_info:
|
||||
self.service.create_shop(db, shop_data, test_user)
|
||||
|
||||
def test_can_view_shop_active_verified(self, test_user, verified_shop):
|
||||
"""Test can_view_shop returns True for active verified shop"""
|
||||
can_view = self.service.can_view_shop(verified_shop, test_user)
|
||||
exception = exc_info.value
|
||||
exception_dict = exception.to_dict()
|
||||
|
||||
assert can_view is True
|
||||
# Verify structure matches expected API response format
|
||||
assert "error_code" in exception_dict
|
||||
assert "message" in exception_dict
|
||||
assert "status_code" in exception_dict
|
||||
assert "details" in exception_dict
|
||||
|
||||
def test_can_view_shop_inactive_unverified(self, test_user, inactive_shop):
|
||||
"""Test can_view_shop returns False for inactive unverified shop"""
|
||||
can_view = self.service.can_view_shop(inactive_shop, test_user)
|
||||
# Verify values
|
||||
assert exception_dict["error_code"] == "SHOP_ALREADY_EXISTS"
|
||||
assert exception_dict["status_code"] == 409
|
||||
assert isinstance(exception_dict["details"], dict)
|
||||
|
||||
assert can_view is False
|
||||
def test_validation_exception_field_details(self, db, test_user):
|
||||
"""Test validation exceptions include field-specific details"""
|
||||
shop_data = ShopCreate(shop_code="", shop_name="Test")
|
||||
|
||||
with pytest.raises(InvalidShopDataException) as exc_info:
|
||||
self.service.create_shop(db, shop_data, test_user)
|
||||
|
||||
exception = exc_info.value
|
||||
assert exception.details["field"] == "shop_code"
|
||||
assert exception.status_code == 422
|
||||
assert "required" in exception.message.lower()
|
||||
|
||||
def test_authorization_exception_user_details(self, db, test_user, inactive_shop):
|
||||
"""Test authorization exceptions include user context"""
|
||||
with pytest.raises(UnauthorizedShopAccessException) as exc_info:
|
||||
self.service.get_shop_by_code(db, inactive_shop.shop_code, test_user)
|
||||
|
||||
exception = exc_info.value
|
||||
assert exception.details["shop_code"] == inactive_shop.shop_code
|
||||
assert exception.details["user_id"] == test_user.id
|
||||
assert "Unauthorized access" in exception.message
|
||||
|
||||
Reference in New Issue
Block a user