366 lines
15 KiB
Python
366 lines
15 KiB
Python
# tests/test_vendor_service.py (updated to use custom exceptions)
|
|
import pytest
|
|
|
|
from app.services.vendor_service import VendorService
|
|
from app.exceptions import (
|
|
VendorNotFoundException,
|
|
VendorAlreadyExistsException,
|
|
UnauthorizedVendorAccessException,
|
|
InvalidVendorDataException,
|
|
MarketplaceProductNotFoundException,
|
|
ProductAlreadyExistsException,
|
|
MaxVendorsReachedException,
|
|
ValidationException,
|
|
)
|
|
from models.schemas.vendor import VendorCreate
|
|
from models.schemas.product import ProductCreate
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.vendors
|
|
class TestVendorService:
|
|
"""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 = VendorService()
|
|
|
|
def test_create_vendor_success(self, db, test_user, vendor_factory):
|
|
"""Test successful vendor creation"""
|
|
vendor_data = VendorCreate(
|
|
vendor_code="NEWVENDOR",
|
|
vendor_name="New Test Shop",
|
|
description="A new test vendor ",
|
|
)
|
|
|
|
vendor = self.service.create_vendor(db, vendor_data, test_user)
|
|
|
|
assert vendor is not None
|
|
assert vendor.vendor_code == "NEWVENDOR"
|
|
assert vendor.owner_id == test_user.id
|
|
assert vendor.is_verified is False # Regular user creates unverified vendor
|
|
|
|
def test_create_vendor_admin_auto_verify(self, db, test_admin, vendor_factory):
|
|
"""Test admin creates verified vendor automatically"""
|
|
vendor_data = VendorCreate(vendor_code="ADMINSHOP", vendor_name="Admin Test Shop")
|
|
|
|
vendor = self.service.create_vendor(db, vendor_data, test_admin)
|
|
|
|
assert vendor.is_verified is True # Admin creates verified vendor
|
|
|
|
def test_create_vendor_duplicate_code(self, db, test_user, test_vendor):
|
|
"""Test vendor creation fails with duplicate vendor code"""
|
|
vendor_data = VendorCreate(
|
|
vendor_code=test_vendor.vendor_code, vendor_name=test_vendor.vendor_name
|
|
)
|
|
|
|
with pytest.raises(VendorAlreadyExistsException) as exc_info:
|
|
self.service.create_vendor(db, vendor_data, test_user)
|
|
|
|
exception = exc_info.value
|
|
assert exception.status_code == 409
|
|
assert exception.error_code == "VENDOR_ALREADY_EXISTS"
|
|
assert test_vendor.vendor_code.upper() in exception.message
|
|
assert "vendor_code" in exception.details
|
|
|
|
def test_create_vendor_invalid_data_empty_code(self, db, test_user):
|
|
"""Test vendor creation fails with empty vendor code"""
|
|
vendor_data = VendorCreate(vendor_code="", vendor_name="Test Shop")
|
|
|
|
with pytest.raises(InvalidVendorDataException) as exc_info:
|
|
self.service.create_vendor(db, vendor_data, test_user)
|
|
|
|
exception = exc_info.value
|
|
assert exception.status_code == 422
|
|
assert exception.error_code == "INVALID_VENDOR_DATA"
|
|
assert exception.details["field"] == "vendor_code"
|
|
|
|
def test_create_vendor_invalid_data_empty_name(self, db, test_user):
|
|
"""Test vendor creation fails with empty vendor name"""
|
|
vendor_data = VendorCreate(vendor_code="VALIDCODE", vendor_name="")
|
|
|
|
with pytest.raises(InvalidVendorDataException) as exc_info:
|
|
self.service.create_vendor(db, vendor_data, test_user)
|
|
|
|
exception = exc_info.value
|
|
assert exception.error_code == "INVALID_VENDOR_DATA"
|
|
assert exception.details["field"] == "vendor_name"
|
|
|
|
def test_create_vendor_invalid_code_format(self, db, test_user):
|
|
"""Test vendor creation fails with invalid vendor code format"""
|
|
vendor_data = VendorCreate(vendor_code="INVALID@CODE!", vendor_name="Test Shop")
|
|
|
|
with pytest.raises(InvalidVendorDataException) as exc_info:
|
|
self.service.create_vendor(db, vendor_data, test_user)
|
|
|
|
exception = exc_info.value
|
|
assert exception.error_code == "INVALID_VENDOR_DATA"
|
|
assert exception.details["field"] == "vendor_code"
|
|
assert "letters, numbers, underscores, and hyphens" in exception.message
|
|
|
|
def test_create_vendor_max_vendors_reached(self, db, test_user, monkeypatch):
|
|
"""Test vendor creation fails when user reaches maximum vendors"""
|
|
|
|
# Mock the vendor count check to simulate user at limit
|
|
def mock_check_vendor_limit(self, db, user):
|
|
raise MaxVendorsReachedException(max_vendors=5, user_id=user.id)
|
|
|
|
monkeypatch.setattr(VendorService, "_check_vendor_limit", mock_check_vendor_limit)
|
|
|
|
vendor_data = VendorCreate(vendor_code="NEWVENDOR", vendor_name="New Vendor")
|
|
|
|
with pytest.raises(MaxVendorsReachedException) as exc_info:
|
|
self.service.create_vendor(db, vendor_data, test_user)
|
|
|
|
exception = exc_info.value
|
|
assert exception.status_code == 400
|
|
assert exception.error_code == "MAX_VENDORS_REACHED"
|
|
assert exception.details["max_vendors"] == 5
|
|
assert exception.details["user_id"] == test_user.id
|
|
|
|
def test_get_vendors_regular_user(self, db, test_user, test_vendor, inactive_vendor):
|
|
"""Test regular user can only see active verified vendors and own vendors"""
|
|
vendors, total = self.service.get_vendors(db, test_user, skip=0, limit=10)
|
|
|
|
vendor_codes = [vendor.vendor_code for vendor in vendors]
|
|
assert test_vendor.vendor_code in vendor_codes
|
|
assert inactive_vendor.vendor_code not in vendor_codes
|
|
|
|
def test_get_vendors_admin_user(
|
|
self, db, test_admin, test_vendor, inactive_vendor, verified_vendor
|
|
):
|
|
"""Test admin user can see all vendors with filters"""
|
|
vendors, total = self.service.get_vendors(
|
|
db, test_admin, active_only=False, verified_only=False
|
|
)
|
|
|
|
vendor_codes = [vendor.vendor_code for vendor in vendors]
|
|
assert test_vendor.vendor_code in vendor_codes
|
|
assert inactive_vendor.vendor_code in vendor_codes
|
|
assert verified_vendor.vendor_code in vendor_codes
|
|
|
|
def test_get_vendor_by_code_owner_access(self, db, test_user, test_vendor):
|
|
"""Test vendor owner can access their own vendor """
|
|
vendor = self.service.get_vendor_by_code(db, test_vendor.vendor_code.lower(), test_user)
|
|
|
|
assert vendor is not None
|
|
assert vendor.id == test_vendor.id
|
|
|
|
def test_get_vendor_by_code_admin_access(self, db, test_admin, test_vendor):
|
|
"""Test admin can access any vendor """
|
|
vendor = self.service.get_vendor_by_code(
|
|
db, test_vendor.vendor_code.lower(), test_admin
|
|
)
|
|
|
|
assert vendor is not None
|
|
assert vendor.id == test_vendor.id
|
|
|
|
def test_get_vendor_by_code_not_found(self, db, test_user):
|
|
"""Test vendor not found raises proper exception"""
|
|
with pytest.raises(VendorNotFoundException) as exc_info:
|
|
self.service.get_vendor_by_code(db, "NONEXISTENT", test_user)
|
|
|
|
exception = exc_info.value
|
|
assert exception.status_code == 404
|
|
assert exception.error_code == "VENDOR_NOT_FOUND"
|
|
assert exception.details["resource_type"] == "Shop"
|
|
assert exception.details["identifier"] == "NONEXISTENT"
|
|
|
|
def test_get_vendor_by_code_access_denied(self, db, test_user, inactive_vendor):
|
|
"""Test regular user cannot access unverified vendor they don't own"""
|
|
with pytest.raises(UnauthorizedVendorAccessException) as exc_info:
|
|
self.service.get_vendor_by_code(db, inactive_vendor.vendor_code, test_user)
|
|
|
|
exception = exc_info.value
|
|
assert exception.status_code == 403
|
|
assert exception.error_code == "UNAUTHORIZED_VENDOR_ACCESS"
|
|
assert exception.details["vendor_code"] == inactive_vendor.vendor_code
|
|
assert exception.details["user_id"] == test_user.id
|
|
|
|
def test_add_product_to_vendor_success(self, db, test_vendor, unique_product):
|
|
"""Test successfully adding product to vendor """
|
|
product_data = ProductCreate(
|
|
marketplace_product_id=unique_product.marketplace_product_id,
|
|
price="15.99",
|
|
is_featured=True,
|
|
)
|
|
|
|
product = self.service.add_product_to_catalog(
|
|
db, test_vendor, product_data
|
|
)
|
|
|
|
assert product is not None
|
|
assert product.vendor_id == test_vendor.id
|
|
assert product.marketplace_product_id == unique_product.id
|
|
|
|
def test_add_product_to_vendor_product_not_found(self, db, test_vendor):
|
|
"""Test adding non-existent product to vendor fails"""
|
|
product_data = ProductCreate(marketplace_product_id="NONEXISTENT", price="15.99")
|
|
|
|
with pytest.raises(MarketplaceProductNotFoundException) as exc_info:
|
|
self.service.add_product_to_catalog(db, test_vendor, product_data)
|
|
|
|
exception = exc_info.value
|
|
assert exception.status_code == 404
|
|
assert exception.error_code == "PRODUCT_NOT_FOUND"
|
|
assert exception.details["resource_type"] == "MarketplaceProduct"
|
|
assert exception.details["identifier"] == "NONEXISTENT"
|
|
|
|
def test_add_product_to_vendor_already_exists(self, db, test_vendor, test_product):
|
|
"""Test adding product that's already in vendor fails"""
|
|
product_data = ProductCreate(
|
|
marketplace_product_id=test_product.marketplace_product.marketplace_product_id, price="15.99"
|
|
)
|
|
|
|
with pytest.raises(ProductAlreadyExistsException) as exc_info:
|
|
self.service.add_product_to_catalog(db, test_vendor, product_data)
|
|
|
|
exception = exc_info.value
|
|
assert exception.status_code == 409
|
|
assert exception.error_code == "PRODUCT_ALREADY_EXISTS"
|
|
assert exception.details["vendor_code"] == test_vendor.vendor_code
|
|
assert exception.details["marketplace_product_id"] == test_product.marketplace_product.marketplace_product_id
|
|
|
|
def test_get_products_owner_access(
|
|
self, db, test_user, test_vendor, test_product
|
|
):
|
|
"""Test vendor owner can get vendor products"""
|
|
products, total = self.service.get_products(db, test_vendor, test_user)
|
|
|
|
assert total >= 1
|
|
assert len(products) >= 1
|
|
product_ids = [p.marketplace_product_id for p in products]
|
|
assert test_product.marketplace_product_id in product_ids
|
|
|
|
def test_get_products_access_denied(self, db, test_user, inactive_vendor):
|
|
"""Test non-owner cannot access unverified vendor products"""
|
|
with pytest.raises(UnauthorizedVendorAccessException) as exc_info:
|
|
self.service.get_products(db, inactive_vendor, test_user)
|
|
|
|
exception = exc_info.value
|
|
assert exception.status_code == 403
|
|
assert exception.error_code == "UNAUTHORIZED_VENDOR_ACCESS"
|
|
assert exception.details["vendor_code"] == inactive_vendor.vendor_code
|
|
assert exception.details["user_id"] == test_user.id
|
|
|
|
def test_get_products_with_filters(self, db, test_user, test_vendor, test_product):
|
|
"""Test getting vendor products with various filters"""
|
|
# Test active only filter
|
|
products, total = self.service.get_products(
|
|
db, test_vendor, test_user, active_only=True
|
|
)
|
|
assert all(p.is_active for p in products)
|
|
|
|
# Test featured only filter
|
|
products, total = self.service.get_products(
|
|
db, test_vendor, test_user, featured_only=True
|
|
)
|
|
assert all(p.is_featured for p in products)
|
|
|
|
# Test exception handling for generic errors
|
|
def test_create_vendor_database_error(self, db, test_user, monkeypatch):
|
|
"""Test vendor creation handles database errors gracefully"""
|
|
|
|
def mock_commit():
|
|
raise Exception("Database connection failed")
|
|
|
|
monkeypatch.setattr(db, "commit", mock_commit)
|
|
|
|
vendor_data = VendorCreate(vendor_code="NEWVENDOR", vendor_name="Test Shop")
|
|
|
|
with pytest.raises(ValidationException) as exc_info:
|
|
self.service.create_vendor(db, vendor_data, test_user)
|
|
|
|
exception = exc_info.value
|
|
assert exception.status_code == 422
|
|
assert exception.error_code == "VALIDATION_ERROR"
|
|
assert "Failed to create vendor " in exception.message
|
|
|
|
def test_get_vendors_database_error(self, db, test_user, monkeypatch):
|
|
"""Test get vendors 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_vendors(db, test_user)
|
|
|
|
exception = exc_info.value
|
|
assert exception.error_code == "VALIDATION_ERROR"
|
|
assert "Failed to retrieve vendors" in exception.message
|
|
|
|
def test_add_product_database_error(self, db, test_vendor, unique_product, monkeypatch):
|
|
"""Test add product handles database errors gracefully"""
|
|
|
|
def mock_commit():
|
|
raise Exception("Database commit failed")
|
|
|
|
monkeypatch.setattr(db, "commit", mock_commit)
|
|
|
|
product_data = ProductCreate(
|
|
marketplace_product_id=unique_product.marketplace_product_id, price="15.99"
|
|
)
|
|
|
|
with pytest.raises(ValidationException) as exc_info:
|
|
self.service.add_product_to_catalog(db, test_vendor, product_data)
|
|
|
|
exception = exc_info.value
|
|
assert exception.error_code == "VALIDATION_ERROR"
|
|
assert "Failed to add product to vendor " in exception.message
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.vendors
|
|
class TestVendorServiceExceptionDetails:
|
|
"""Additional tests focusing specifically on exception structure and details"""
|
|
|
|
def setup_method(self):
|
|
self.service = VendorService()
|
|
|
|
def test_exception_to_dict_structure(self, db, test_user, test_vendor):
|
|
"""Test that exceptions can be properly serialized to dict for API responses"""
|
|
vendor_data = VendorCreate(
|
|
vendor_code=test_vendor.vendor_code, vendor_name="Duplicate"
|
|
)
|
|
|
|
with pytest.raises(VendorAlreadyExistsException) as exc_info:
|
|
self.service.create_vendor(db, vendor_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"] == "VENDOR_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"""
|
|
vendor_data = VendorCreate(vendor_code="", vendor_name="Test")
|
|
|
|
with pytest.raises(InvalidVendorDataException) as exc_info:
|
|
self.service.create_vendor(db, vendor_data, test_user)
|
|
|
|
exception = exc_info.value
|
|
assert exception.details["field"] == "vendor_code"
|
|
assert exception.status_code == 422
|
|
assert "required" in exception.message.lower()
|
|
|
|
def test_authorization_exception_user_details(self, db, test_user, inactive_vendor):
|
|
"""Test authorization exceptions include user context"""
|
|
with pytest.raises(UnauthorizedVendorAccessException) as exc_info:
|
|
self.service.get_vendor_by_code(db, inactive_vendor.vendor_code, test_user)
|
|
|
|
exception = exc_info.value
|
|
assert exception.details["vendor_code"] == inactive_vendor.vendor_code
|
|
assert exception.details["user_id"] == test_user.id
|
|
assert "Unauthorized access" in exception.message
|