Files
orion/tests/unit/services/test_vendor_service.py
Samir Boulahtit e21630d63a test: add service unit tests to improve coverage
- Add tests for inventory_service.py (admin methods, helpers)
- Add tests for vendor_service.py (identifier, permissions, updates)
- Add tests for marketplace_product_service.py (CRUD, admin, CSV export)
- Add tests for order_service.py (number generation, customer mgmt)

Coverage improved from 67% to 69.6%

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 17:19:38 +01:00

729 lines
28 KiB
Python

# tests/unit/services/test_vendor_service.py
"""Unit tests for VendorService following the application's exception patterns."""
import uuid
import pytest
from app.exceptions import (
InvalidVendorDataException,
MarketplaceProductNotFoundException,
ProductAlreadyExistsException,
UnauthorizedVendorAccessException,
ValidationException,
VendorAlreadyExistsException,
VendorNotFoundException,
)
from app.services.vendor_service import VendorService
from models.database.company import Company
from models.database.vendor import Vendor
from models.schema.product import ProductCreate
from models.schema.vendor import VendorCreate
@pytest.fixture
def admin_company(db, test_admin):
"""Create a test company for admin."""
unique_id = str(uuid.uuid4())[:8]
company = Company(
name=f"Admin Company {unique_id}",
owner_user_id=test_admin.id,
contact_email=f"admin{unique_id}@company.com",
is_active=True,
is_verified=True,
)
db.add(company)
db.commit()
db.refresh(company)
return company
# Note: other_company fixture is defined in tests/fixtures/vendor_fixtures.py
@pytest.mark.unit
@pytest.mark.vendors
class TestVendorService:
"""Test suite for VendorService following the application's exception patterns."""
def setup_method(self):
"""Setup method following the same pattern as admin service tests."""
self.service = VendorService()
# ==================== create_vendor Tests ====================
def test_create_vendor_success(self, db, test_user, test_company):
"""Test successful vendor creation."""
unique_id = str(uuid.uuid4())[:8]
vendor_data = VendorCreate(
company_id=test_company.id,
vendor_code=f"NEWVENDOR_{unique_id}",
subdomain=f"newvendor{unique_id.lower()}",
name=f"New Test Vendor {unique_id}",
description="A new test vendor",
)
vendor = self.service.create_vendor(db, vendor_data, test_user)
db.commit()
assert vendor is not None
assert vendor.vendor_code == f"NEWVENDOR_{unique_id}".upper()
assert vendor.company_id == test_company.id
assert vendor.is_verified is False # Regular user creates unverified vendor
def test_create_vendor_admin_auto_verify(self, db, test_admin, admin_company):
"""Test admin creates verified vendor automatically."""
unique_id = str(uuid.uuid4())[:8]
vendor_data = VendorCreate(
company_id=admin_company.id,
vendor_code=f"ADMINVENDOR_{unique_id}",
subdomain=f"adminvendor{unique_id.lower()}",
name=f"Admin Test Vendor {unique_id}",
)
vendor = self.service.create_vendor(db, vendor_data, test_admin)
db.commit()
assert vendor.is_verified is True # Admin creates verified vendor
def test_create_vendor_duplicate_code(
self, db, test_user, test_company, test_vendor
):
"""Test vendor creation fails with duplicate vendor code."""
vendor_data = VendorCreate(
company_id=test_company.id,
vendor_code=test_vendor.vendor_code,
subdomain="duplicatesub",
name="Duplicate 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
def test_create_vendor_missing_company_id(self, db, test_user):
"""Test vendor creation fails without company_id."""
# VendorCreate requires company_id, so this should raise ValidationError
# from Pydantic before reaching service
with pytest.raises(Exception): # Pydantic ValidationError
VendorCreate(
vendor_code="NOCOMPANY",
subdomain="nocompany",
name="No Company Vendor",
)
def test_create_vendor_unauthorized_user(self, db, test_user, other_company):
"""Test vendor creation fails when user doesn't own company."""
unique_id = str(uuid.uuid4())[:8]
vendor_data = VendorCreate(
company_id=other_company.id, # Not owned by test_user
vendor_code=f"UNAUTH_{unique_id}",
subdomain=f"unauth{unique_id.lower()}",
name=f"Unauthorized Vendor {unique_id}",
)
with pytest.raises(UnauthorizedVendorAccessException) as exc_info:
self.service.create_vendor(db, vendor_data, test_user)
exception = exc_info.value
assert exception.status_code == 403
assert exception.error_code == "UNAUTHORIZED_VENDOR_ACCESS"
def test_create_vendor_invalid_company_id(self, db, test_user):
"""Test vendor creation fails with non-existent company."""
unique_id = str(uuid.uuid4())[:8]
vendor_data = VendorCreate(
company_id=99999, # Non-existent company
vendor_code=f"BADCOMPANY_{unique_id}",
subdomain=f"badcompany{unique_id.lower()}",
name=f"Bad Company Vendor {unique_id}",
)
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 "company_id" in exception.details.get("field", "")
# ==================== get_vendors Tests ====================
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=100)
vendor_codes = [vendor.vendor_code for vendor in vendors]
assert test_vendor.vendor_code in vendor_codes
# Inactive vendor should not be visible to regular user
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_vendors_pagination(self, db, test_admin):
"""Test vendor pagination."""
vendors, total = self.service.get_vendors(
db, test_admin, skip=0, limit=5, active_only=False
)
assert len(vendors) <= 5
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
# ==================== get_vendor_by_code Tests ====================
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"
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"
# ==================== get_vendor_by_id Tests ====================
def test_get_vendor_by_id_success(self, db, test_vendor):
"""Test getting vendor by ID."""
vendor = self.service.get_vendor_by_id(db, test_vendor.id)
assert vendor is not None
assert vendor.id == test_vendor.id
assert vendor.vendor_code == test_vendor.vendor_code
def test_get_vendor_by_id_not_found(self, db):
"""Test getting non-existent vendor by ID."""
with pytest.raises(VendorNotFoundException) as exc_info:
self.service.get_vendor_by_id(db, 99999)
exception = exc_info.value
assert exception.status_code == 404
assert exception.error_code == "VENDOR_NOT_FOUND"
# ==================== get_active_vendor_by_code Tests ====================
def test_get_active_vendor_by_code_success(self, db, test_vendor):
"""Test getting active vendor by code (public access)."""
vendor = self.service.get_active_vendor_by_code(db, test_vendor.vendor_code)
assert vendor is not None
assert vendor.id == test_vendor.id
assert vendor.is_active is True
def test_get_active_vendor_by_code_inactive(self, db, inactive_vendor):
"""Test getting inactive vendor fails."""
with pytest.raises(VendorNotFoundException):
self.service.get_active_vendor_by_code(db, inactive_vendor.vendor_code)
def test_get_active_vendor_by_code_not_found(self, db):
"""Test getting non-existent vendor fails."""
with pytest.raises(VendorNotFoundException):
self.service.get_active_vendor_by_code(db, "NONEXISTENT")
# ==================== toggle_verification Tests ====================
def test_toggle_verification_verify(self, db, inactive_vendor):
"""Test toggling verification on."""
original_verified = inactive_vendor.is_verified
vendor, message = self.service.toggle_verification(db, inactive_vendor.id)
db.commit()
assert vendor.is_verified != original_verified
assert "verified" in message.lower()
def test_toggle_verification_unverify(self, db, verified_vendor):
"""Test toggling verification off."""
vendor, message = self.service.toggle_verification(db, verified_vendor.id)
db.commit()
assert vendor.is_verified is False
assert "unverified" in message.lower()
def test_toggle_verification_not_found(self, db):
"""Test toggle verification on non-existent vendor."""
with pytest.raises(VendorNotFoundException):
self.service.toggle_verification(db, 99999)
# ==================== toggle_status Tests ====================
def test_toggle_status_deactivate(self, db, test_vendor):
"""Test toggling active status off."""
vendor, message = self.service.toggle_status(db, test_vendor.id)
db.commit()
assert vendor.is_active is False
assert "inactive" in message.lower()
def test_toggle_status_activate(self, db, inactive_vendor):
"""Test toggling active status on."""
vendor, message = self.service.toggle_status(db, inactive_vendor.id)
db.commit()
assert vendor.is_active is True
assert "active" in message.lower()
def test_toggle_status_not_found(self, db):
"""Test toggle status on non-existent vendor."""
with pytest.raises(VendorNotFoundException):
self.service.toggle_status(db, 99999)
# ==================== set_verification / set_status Tests ====================
def test_set_verification_to_true(self, db, inactive_vendor):
"""Test setting verification to true."""
vendor, message = self.service.set_verification(db, inactive_vendor.id, True)
db.commit()
assert vendor.is_verified is True
def test_set_verification_to_false(self, db, verified_vendor):
"""Test setting verification to false."""
vendor, message = self.service.set_verification(db, verified_vendor.id, False)
db.commit()
assert vendor.is_verified is False
def test_set_status_to_active(self, db, inactive_vendor):
"""Test setting status to active."""
vendor, message = self.service.set_status(db, inactive_vendor.id, True)
db.commit()
assert vendor.is_active is True
def test_set_status_to_inactive(self, db, test_vendor):
"""Test setting status to inactive."""
vendor, message = self.service.set_status(db, test_vendor.id, False)
db.commit()
assert vendor.is_active is False
# ==================== add_product_to_catalog Tests ====================
def test_add_product_to_vendor_success(self, db, test_vendor, unique_product):
"""Test successfully adding product to vendor."""
from models.database.marketplace_product import MarketplaceProduct
# Re-query objects to avoid session issues
vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first()
mp = (
db.query(MarketplaceProduct)
.filter(MarketplaceProduct.id == unique_product.id)
.first()
)
product_data = ProductCreate(
marketplace_product_id=mp.id,
price=15.99,
is_featured=True,
)
product = self.service.add_product_to_catalog(db, vendor, product_data)
db.commit()
assert product is not None
assert product.vendor_id == vendor.id
assert product.marketplace_product_id == mp.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=99999, # Non-existent ID
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"
def test_add_product_to_vendor_already_exists(self, db, test_vendor, test_product):
"""Test adding product that's already in vendor fails."""
# Re-query to get fresh instances
from models.database.product import Product
vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first()
product = db.query(Product).filter(Product.id == test_product.id).first()
product_data = ProductCreate(
marketplace_product_id=product.marketplace_product_id,
price=15.99,
)
with pytest.raises(ProductAlreadyExistsException) as exc_info:
self.service.add_product_to_catalog(db, vendor, product_data)
exception = exc_info.value
assert exception.status_code == 409
assert exception.error_code == "PRODUCT_ALREADY_EXISTS"
# ==================== get_products Tests ====================
def test_get_products_owner_access(self, db, test_user, test_vendor, test_product):
"""Test vendor owner can get vendor products."""
# Re-query vendor to get fresh instance
vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first()
products, total = self.service.get_products(db, vendor, test_user)
assert total >= 1
assert len(products) >= 1
def test_get_products_access_denied(self, db, test_user, inactive_vendor):
"""Test non-owner cannot access unverified vendor products."""
# Re-query vendor to get fresh instance
vendor = db.query(Vendor).filter(Vendor.id == inactive_vendor.id).first()
with pytest.raises(UnauthorizedVendorAccessException) as exc_info:
self.service.get_products(db, vendor, test_user)
exception = exc_info.value
assert exception.status_code == 403
assert exception.error_code == "UNAUTHORIZED_VENDOR_ACCESS"
def test_get_products_with_filters(self, db, test_user, test_vendor, test_product):
"""Test getting vendor products with various filters."""
# Re-query vendor to get fresh instance
vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first()
# Test active only filter
products, total = self.service.get_products(
db, vendor, test_user, active_only=True
)
assert all(p.is_active for p in products)
# ==================== Helper Method Tests ====================
def test_vendor_code_exists(self, db, test_vendor):
"""Test _vendor_code_exists helper method."""
assert self.service._vendor_code_exists(db, test_vendor.vendor_code) is True
assert self.service._vendor_code_exists(db, "NONEXISTENT") is False
def test_can_access_vendor_admin(self, db, test_admin, test_vendor):
"""Test admin can always access vendor."""
# Re-query vendor to get fresh instance
vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first()
assert self.service._can_access_vendor(vendor, test_admin) is True
def test_can_access_vendor_active_verified(self, db, test_user, verified_vendor):
"""Test any user can access active verified vendor."""
# Re-query vendor to get fresh instance
vendor = db.query(Vendor).filter(Vendor.id == verified_vendor.id).first()
assert self.service._can_access_vendor(vendor, test_user) is True
@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_company
):
"""Test that exceptions can be properly serialized to dict for API responses."""
vendor_data = VendorCreate(
company_id=test_company.id,
vendor_code=test_vendor.vendor_code,
subdomain="duplicate",
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_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
def test_not_found_exception_details(self, db, test_user):
"""Test not found exceptions include identifier details."""
with pytest.raises(VendorNotFoundException) as exc_info:
self.service.get_vendor_by_code(db, "NOTEXIST", test_user)
exception = exc_info.value
assert exception.status_code == 404
assert exception.error_code == "VENDOR_NOT_FOUND"
@pytest.mark.unit
@pytest.mark.vendors
class TestVendorServiceIdentifier:
"""Tests for get_vendor_by_identifier method."""
def setup_method(self):
self.service = VendorService()
def test_get_vendor_by_identifier_with_id(self, db, test_vendor):
"""Test getting vendor by numeric ID string."""
vendor = self.service.get_vendor_by_identifier(db, str(test_vendor.id))
assert vendor is not None
assert vendor.id == test_vendor.id
def test_get_vendor_by_identifier_with_code(self, db, test_vendor):
"""Test getting vendor by vendor_code."""
vendor = self.service.get_vendor_by_identifier(db, test_vendor.vendor_code)
assert vendor is not None
assert vendor.vendor_code == test_vendor.vendor_code
def test_get_vendor_by_identifier_case_insensitive(self, db, test_vendor):
"""Test getting vendor by vendor_code is case insensitive."""
vendor = self.service.get_vendor_by_identifier(
db, test_vendor.vendor_code.lower()
)
assert vendor is not None
assert vendor.id == test_vendor.id
def test_get_vendor_by_identifier_not_found(self, db):
"""Test getting non-existent vendor."""
with pytest.raises(VendorNotFoundException):
self.service.get_vendor_by_identifier(db, "NONEXISTENT_CODE")
@pytest.mark.unit
@pytest.mark.vendors
class TestVendorServicePermissions:
"""Tests for permission checking methods."""
def setup_method(self):
self.service = VendorService()
def test_can_update_vendor_admin(self, db, test_admin, test_vendor):
"""Test admin can always update vendor."""
vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first()
assert self.service.can_update_vendor(vendor, test_admin) is True
def test_can_update_vendor_owner(self, db, test_user, test_vendor):
"""Test owner can update vendor."""
vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first()
assert self.service.can_update_vendor(vendor, test_user) is True
def test_can_update_vendor_non_owner(self, db, other_company, test_vendor):
"""Test non-owner cannot update vendor."""
from models.database.user import User
vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first()
other_user = db.query(User).filter(User.id == other_company.owner_user_id).first()
# Clear any VendorUser relationships
assert self.service.can_update_vendor(vendor, other_user) is False
def test_is_vendor_owner_true(self, db, test_user, test_vendor):
"""Test _is_vendor_owner returns True for owner."""
vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first()
assert self.service._is_vendor_owner(vendor, test_user) is True
def test_is_vendor_owner_false(self, db, other_company, test_vendor):
"""Test _is_vendor_owner returns False for non-owner."""
from models.database.user import User
vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first()
other_user = db.query(User).filter(User.id == other_company.owner_user_id).first()
assert self.service._is_vendor_owner(vendor, other_user) is False
@pytest.mark.unit
@pytest.mark.vendors
class TestVendorServiceUpdate:
"""Tests for update methods."""
def setup_method(self):
self.service = VendorService()
def test_update_vendor_success(self, db, test_user, test_vendor):
"""Test successfully updating vendor profile."""
from pydantic import BaseModel
class VendorUpdate(BaseModel):
name: str | None = None
description: str | None = None
class Config:
extra = "forbid"
update_data = VendorUpdate(
name="Updated Vendor Name",
description="Updated description",
)
vendor = self.service.update_vendor(
db, test_vendor.id, update_data, test_user
)
db.commit()
assert vendor.name == "Updated Vendor Name"
assert vendor.description == "Updated description"
def test_update_vendor_unauthorized(self, db, other_company, test_vendor):
"""Test update fails for unauthorized user."""
from pydantic import BaseModel
from app.exceptions import InsufficientPermissionsException
from models.database.user import User
class VendorUpdate(BaseModel):
name: str | None = None
class Config:
extra = "forbid"
other_user = db.query(User).filter(User.id == other_company.owner_user_id).first()
update_data = VendorUpdate(name="Unauthorized Update")
with pytest.raises(InsufficientPermissionsException):
self.service.update_vendor(
db, test_vendor.id, update_data, other_user
)
def test_update_vendor_not_found(self, db, test_admin):
"""Test update fails for non-existent vendor."""
from pydantic import BaseModel
class VendorUpdate(BaseModel):
name: str | None = None
class Config:
extra = "forbid"
update_data = VendorUpdate(name="Update")
with pytest.raises(VendorNotFoundException):
self.service.update_vendor(db, 99999, update_data, test_admin)
def test_update_marketplace_settings_success(self, db, test_user, test_vendor):
"""Test successfully updating marketplace settings."""
marketplace_config = {
"letzshop_csv_url_fr": "https://example.com/fr.csv",
"letzshop_csv_url_en": "https://example.com/en.csv",
}
result = self.service.update_marketplace_settings(
db, test_vendor.id, marketplace_config, test_user
)
db.commit()
assert result["message"] == "Marketplace settings updated successfully"
assert result["letzshop_csv_url_fr"] == "https://example.com/fr.csv"
assert result["letzshop_csv_url_en"] == "https://example.com/en.csv"
def test_update_marketplace_settings_unauthorized(
self, db, other_company, test_vendor
):
"""Test marketplace settings update fails for unauthorized user."""
from app.exceptions import InsufficientPermissionsException
from models.database.user import User
other_user = db.query(User).filter(User.id == other_company.owner_user_id).first()
marketplace_config = {"letzshop_csv_url_fr": "https://example.com/fr.csv"}
with pytest.raises(InsufficientPermissionsException):
self.service.update_marketplace_settings(
db, test_vendor.id, marketplace_config, other_user
)
def test_update_marketplace_settings_not_found(self, db, test_admin):
"""Test marketplace settings update fails for non-existent vendor."""
marketplace_config = {"letzshop_csv_url_fr": "https://example.com/fr.csv"}
with pytest.raises(VendorNotFoundException):
self.service.update_marketplace_settings(
db, 99999, marketplace_config, test_admin
)
@pytest.mark.unit
@pytest.mark.vendors
class TestVendorServiceSingleton:
"""Test singleton instance."""
def test_singleton_exists(self):
"""Test vendor_service singleton exists."""
from app.services.vendor_service import vendor_service
assert vendor_service is not None
assert isinstance(vendor_service, VendorService)