Files
orion/tests/unit/services/test_shop_service.py

366 lines
15 KiB
Python

# tests/test_shop_service.py (updated to use custom exceptions)
import pytest
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 exception patterns"""
def setup_method(self):
"""Setup method following the same pattern as admin service tests"""
self.service = ShopService()
def test_create_shop_success(self, db, test_user, shop_factory):
"""Test successful shop creation"""
shop_data = ShopCreate(
shop_code="NEWSHOP",
shop_name="New Test Shop",
description="A new test shop",
)
shop = self.service.create_shop(db, shop_data, test_user)
assert shop is not None
assert shop.shop_code == "NEWSHOP"
assert shop.owner_id == test_user.id
assert shop.is_verified is False # Regular user creates unverified shop
def test_create_shop_admin_auto_verify(self, db, test_admin, shop_factory):
"""Test admin creates verified shop automatically"""
shop_data = ShopCreate(shop_code="ADMINSHOP", shop_name="Admin Test Shop")
shop = self.service.create_shop(db, shop_data, test_admin)
assert shop.is_verified is True # Admin creates verified shop
def test_create_shop_duplicate_code(self, db, test_user, test_shop):
"""Test shop creation fails with duplicate shop code"""
shop_data = ShopCreate(
shop_code=test_shop.shop_code, shop_name=test_shop.shop_name
)
with pytest.raises(ShopAlreadyExistsException) as exc_info:
self.service.create_shop(db, shop_data, test_user)
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"""
shops, total = self.service.get_shops(db, test_user, skip=0, limit=10)
shop_codes = [shop.shop_code for shop in shops]
assert test_shop.shop_code in shop_codes
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
):
"""Test admin user can see all shops with filters"""
shops, total = self.service.get_shops(
db, test_admin, active_only=False, verified_only=False
)
shop_codes = [shop.shop_code for shop in shops]
assert test_shop.shop_code in shop_codes
assert inactive_shop.shop_code in shop_codes
assert verified_shop.shop_code in shop_codes
def test_get_shop_by_code_owner_access(self, db, test_user, test_shop):
"""Test shop owner can access their own shop"""
shop = self.service.get_shop_by_code(db, test_shop.shop_code.lower(), test_user)
assert shop is not None
assert shop.id == test_shop.id
def test_get_shop_by_code_admin_access(self, db, test_admin, test_shop):
"""Test admin can access any shop"""
shop = self.service.get_shop_by_code(
db, test_shop.shop_code.lower(), test_admin
)
assert shop is not None
assert shop.id == test_shop.id
def test_get_shop_by_code_not_found(self, db, test_user):
"""Test shop not found raises proper exception"""
with pytest.raises(ShopNotFoundException) as exc_info:
self.service.get_shop_by_code(db, "NONEXISTENT", test_user)
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(UnauthorizedShopAccessException) as exc_info:
self.service.get_shop_by_code(db, inactive_shop.shop_code, test_user)
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"""
shop_product_data = ShopProductCreate(
product_id=unique_product.product_id,
price="15.99",
is_featured=True,
stock_quantity=5,
)
shop_product = self.service.add_product_to_shop(
db, test_shop, shop_product_data
)
assert shop_product is not None
assert shop_product.shop_id == test_shop.id
assert shop_product.product_id == unique_product.id
def test_add_product_to_shop_product_not_found(self, db, test_shop):
"""Test adding non-existent product to shop fails"""
shop_product_data = ShopProductCreate(product_id="NONEXISTENT", price="15.99")
with pytest.raises(ProductNotFoundException) as exc_info:
self.service.add_product_to_shop(db, test_shop, shop_product_data)
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"""
shop_product_data = ShopProductCreate(
product_id=shop_product.product.product_id, price="15.99"
)
with pytest.raises(ShopProductAlreadyExistsException) as exc_info:
self.service.add_product_to_shop(db, test_shop, shop_product_data)
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
):
"""Test shop owner can get shop products"""
products, total = self.service.get_shop_products(db, test_shop, test_user)
assert total >= 1
assert len(products) >= 1
product_ids = [p.product_id for p in products]
assert shop_product.product_id in product_ids
def test_get_shop_products_access_denied(self, db, test_user, inactive_shop):
"""Test non-owner cannot access unverified shop products"""
with pytest.raises(UnauthorizedShopAccessException) as exc_info:
self.service.get_shop_products(db, inactive_shop, test_user)
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_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)
# 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)
# Test exception handling for generic errors
def test_create_shop_database_error(self, db, test_user, monkeypatch):
"""Test shop creation handles database errors gracefully"""
def mock_commit():
raise Exception("Database connection failed")
monkeypatch.setattr(db, "commit", mock_commit)
shop_data = ShopCreate(shop_code="NEWSHOP", shop_name="Test Shop")
with pytest.raises(ValidationException) 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 == "VALIDATION_ERROR"
assert "Failed to create shop" in exception.message
def test_get_shops_database_error(self, db, test_user, monkeypatch):
"""Test get shops handles database errors gracefully"""
def mock_query(*args):
raise Exception("Database query failed")
monkeypatch.setattr(db, "query", mock_query)
with pytest.raises(ValidationException) as exc_info:
self.service.get_shops(db, test_user)
exception = exc_info.value
assert exception.error_code == "VALIDATION_ERROR"
assert "Failed to retrieve shops" in exception.message
def test_add_product_database_error(self, db, test_shop, unique_product, monkeypatch):
"""Test add product handles database errors gracefully"""
def mock_commit():
raise Exception("Database commit failed")
monkeypatch.setattr(db, "commit", mock_commit)
shop_product_data = ShopProductCreate(
product_id=unique_product.product_id, price="15.99"
)
with pytest.raises(ValidationException) as exc_info:
self.service.add_product_to_shop(db, test_shop, shop_product_data)
exception = exc_info.value
assert exception.error_code == "VALIDATION_ERROR"
assert "Failed to add product to shop" in exception.message
@pytest.mark.unit
@pytest.mark.shops
class TestShopServiceExceptionDetails:
"""Additional tests focusing specifically on exception structure and details"""
def setup_method(self):
self.service = ShopService()
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"
)
with pytest.raises(ShopAlreadyExistsException) as exc_info:
self.service.create_shop(db, shop_data, test_user)
exception = exc_info.value
exception_dict = exception.to_dict()
# 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
# Verify values
assert exception_dict["error_code"] == "SHOP_ALREADY_EXISTS"
assert exception_dict["status_code"] == 409
assert isinstance(exception_dict["details"], dict)
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