test updates to take into account exception management
This commit is contained in:
@@ -163,25 +163,25 @@ class TestAuthService:
|
||||
|
||||
def test_email_exists_true(self, db, test_user):
|
||||
"""Test email_exists returns True when email exists"""
|
||||
exists = self.service.email_exists(db, test_user.email)
|
||||
exists = self.service._email_exists(db, test_user.email)
|
||||
|
||||
assert exists is True
|
||||
|
||||
def test_email_exists_false(self, db):
|
||||
"""Test email_exists returns False when email doesn't exist"""
|
||||
exists = self.service.email_exists(db, "nonexistent@example.com")
|
||||
exists = self.service._email_exists(db, "nonexistent@example.com")
|
||||
|
||||
assert exists is False
|
||||
|
||||
def test_username_exists_true(self, db, test_user):
|
||||
"""Test username_exists returns True when username exists"""
|
||||
exists = self.service.username_exists(db, test_user.username)
|
||||
exists = self.service._username_exists(db, test_user.username)
|
||||
|
||||
assert exists is True
|
||||
|
||||
def test_username_exists_false(self, db):
|
||||
"""Test username_exists returns False when username doesn't exist"""
|
||||
exists = self.service.username_exists(db, "nonexistentuser")
|
||||
exists = self.service._username_exists(db, "nonexistentuser")
|
||||
|
||||
assert exists is False
|
||||
|
||||
|
||||
@@ -231,10 +231,13 @@ class TestProductService:
|
||||
|
||||
def test_get_stock_info_success(self, db, test_product_with_stock):
|
||||
"""Test getting stock info for product with stock"""
|
||||
stock_info = self.service.get_stock_info(db, test_product_with_stock.gtin)
|
||||
# Extract the product from the dictionary
|
||||
product = test_product_with_stock['product']
|
||||
|
||||
stock_info = self.service.get_stock_info(db, product.gtin)
|
||||
|
||||
assert stock_info is not None
|
||||
assert stock_info.gtin == test_product_with_stock.gtin
|
||||
assert stock_info.gtin == product.gtin
|
||||
assert stock_info.total_quantity > 0
|
||||
assert len(stock_info.locations) > 0
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -214,7 +214,7 @@ class TestStatsService:
|
||||
|
||||
def test_get_product_count(self, db, test_product):
|
||||
"""Test getting total product count"""
|
||||
count = self.service.get_product_count(db)
|
||||
count = self.service._get_product_count(db)
|
||||
|
||||
assert count >= 1
|
||||
assert isinstance(count, int)
|
||||
@@ -245,7 +245,7 @@ class TestStatsService:
|
||||
db.add_all(brand_products)
|
||||
db.commit()
|
||||
|
||||
count = self.service.get_unique_brands_count(db)
|
||||
count = self.service._get_unique_brands_count(db)
|
||||
|
||||
assert (
|
||||
count >= 2
|
||||
@@ -278,7 +278,7 @@ class TestStatsService:
|
||||
db.add_all(category_products)
|
||||
db.commit()
|
||||
|
||||
count = self.service.get_unique_categories_count(db)
|
||||
count = self.service._get_unique_categories_count(db)
|
||||
|
||||
assert count >= 2 # At least Electronics and Books
|
||||
assert isinstance(count, int)
|
||||
@@ -307,7 +307,7 @@ class TestStatsService:
|
||||
db.add_all(marketplace_products)
|
||||
db.commit()
|
||||
|
||||
count = self.service.get_unique_marketplaces_count(db)
|
||||
count = self.service._get_unique_marketplaces_count(db)
|
||||
|
||||
assert count >= 2 # At least Amazon and eBay, plus test_product marketplace
|
||||
assert isinstance(count, int)
|
||||
@@ -336,7 +336,7 @@ class TestStatsService:
|
||||
db.add_all(shop_products)
|
||||
db.commit()
|
||||
|
||||
count = self.service.get_unique_shops_count(db)
|
||||
count = self.service._get_unique_shops_count(db)
|
||||
|
||||
assert count >= 2 # At least ShopA and ShopB, plus test_product shop
|
||||
assert isinstance(count, int)
|
||||
@@ -405,7 +405,7 @@ class TestStatsService:
|
||||
db.add_all(marketplace_products)
|
||||
db.commit()
|
||||
|
||||
brands = self.service.get_brands_by_marketplace(db, "SpecificMarket")
|
||||
brands = self.service._get_brands_by_marketplace(db, "SpecificMarket")
|
||||
|
||||
assert len(brands) == 2
|
||||
assert "SpecificBrand1" in brands
|
||||
@@ -438,7 +438,7 @@ class TestStatsService:
|
||||
db.add_all(marketplace_products)
|
||||
db.commit()
|
||||
|
||||
shops = self.service.get_shops_by_marketplace(db, "TestMarketplace")
|
||||
shops = self.service._get_shops_by_marketplace(db, "TestMarketplace")
|
||||
|
||||
assert len(shops) == 2
|
||||
assert "TestShop1" in shops
|
||||
@@ -476,13 +476,13 @@ class TestStatsService:
|
||||
db.add_all(marketplace_products)
|
||||
db.commit()
|
||||
|
||||
count = self.service.get_products_by_marketplace(db, "CountMarketplace")
|
||||
count = self.service._get_products_by_marketplace_count(db, "CountMarketplace")
|
||||
|
||||
assert count == 3
|
||||
|
||||
def test_get_products_by_marketplace_not_found(self, db):
|
||||
"""Test getting product count for non-existent marketplace"""
|
||||
count = self.service.get_products_by_marketplace(db, "NonExistentMarketplace")
|
||||
count = self.service._get_products_by_marketplace_count(db, "NonExistentMarketplace")
|
||||
|
||||
assert count == 0
|
||||
|
||||
|
||||
@@ -4,6 +4,15 @@ import uuid
|
||||
import pytest
|
||||
|
||||
from app.services.stock_service import StockService
|
||||
from app.exceptions import (
|
||||
StockNotFoundException,
|
||||
InsufficientStockException,
|
||||
InvalidStockOperationException,
|
||||
StockValidationException,
|
||||
NegativeStockException,
|
||||
InvalidQuantityException,
|
||||
ValidationException,
|
||||
)
|
||||
from models.schemas.stock import StockAdd, StockCreate, StockUpdate
|
||||
from models.database.product import Product
|
||||
from models.database.stock import Stock
|
||||
@@ -18,72 +27,50 @@ class TestStockService:
|
||||
def test_normalize_gtin_invalid(self):
|
||||
"""Test GTIN normalization with invalid GTINs."""
|
||||
# Completely invalid values that should return None
|
||||
assert self.service.normalize_gtin("invalid") is None
|
||||
assert self.service.normalize_gtin("abcdef") is None
|
||||
assert self.service.normalize_gtin("") is None
|
||||
assert self.service.normalize_gtin(None) is None
|
||||
assert self.service.normalize_gtin(" ") is None # Only whitespace
|
||||
assert self.service.normalize_gtin("!@#$%") is None # Only special characters
|
||||
assert self.service._normalize_gtin("invalid") is None
|
||||
assert self.service._normalize_gtin("abcdef") is None
|
||||
assert self.service._normalize_gtin("") is None
|
||||
assert self.service._normalize_gtin(None) is None
|
||||
assert self.service._normalize_gtin(" ") is None # Only whitespace
|
||||
assert self.service._normalize_gtin("!@#$%") is None # Only special characters
|
||||
|
||||
# Mixed invalid characters that become empty after filtering
|
||||
assert self.service.normalize_gtin("abc-def-ghi") is None # No digits
|
||||
|
||||
# Note: Based on your GTINProcessor implementation, short numeric values
|
||||
# will be padded, not rejected. For example:
|
||||
# - "123" becomes "000000000123" (padded to 12 digits)
|
||||
# - "1" becomes "000000000001" (padded to 12 digits)
|
||||
|
||||
# If you want to test that short GTINs are padded (not rejected):
|
||||
assert self.service.normalize_gtin("123") == "0000000000123"
|
||||
assert self.service.normalize_gtin("1") == "0000000000001"
|
||||
assert self.service.normalize_gtin("12345") == "0000000012345"
|
||||
assert self.service._normalize_gtin("abc-def-ghi") is None # No digits
|
||||
|
||||
def test_normalize_gtin_valid(self):
|
||||
"""Test GTIN normalization with valid GTINs."""
|
||||
# Test various valid GTIN formats - these should remain unchanged
|
||||
assert self.service.normalize_gtin("1234567890123") == "1234567890123" # EAN-13
|
||||
assert self.service.normalize_gtin("123456789012") == "123456789012" # UPC-A
|
||||
assert self.service.normalize_gtin("12345678") == "12345678" # EAN-8
|
||||
assert (
|
||||
self.service.normalize_gtin("12345678901234") == "12345678901234"
|
||||
) # GTIN-14
|
||||
assert self.service._normalize_gtin("1234567890123") == "1234567890123" # EAN-13
|
||||
assert self.service._normalize_gtin("123456789012") == "123456789012" # UPC-A
|
||||
assert self.service._normalize_gtin("12345678") == "12345678" # EAN-8
|
||||
assert self.service._normalize_gtin("12345678901234") == "12345678901234" # GTIN-14
|
||||
|
||||
# Test with decimal points (should be removed)
|
||||
assert self.service.normalize_gtin("1234567890123.0") == "1234567890123"
|
||||
assert self.service._normalize_gtin("1234567890123.0") == "1234567890123"
|
||||
|
||||
# Test with whitespace (should be trimmed)
|
||||
assert self.service.normalize_gtin(" 1234567890123 ") == "1234567890123"
|
||||
assert self.service._normalize_gtin(" 1234567890123 ") == "1234567890123"
|
||||
|
||||
# Test short GTINs being padded
|
||||
assert self.service.normalize_gtin("123") == "0000000000123" # Padded to EAN-13
|
||||
assert (
|
||||
self.service.normalize_gtin("12345") == "0000000012345"
|
||||
) # Padded to EAN-13
|
||||
assert self.service._normalize_gtin("123") == "0000000000123" # Padded to EAN-13
|
||||
assert self.service._normalize_gtin("12345") == "0000000012345" # Padded to EAN-13
|
||||
|
||||
# Test long GTINs being truncated
|
||||
assert (
|
||||
self.service.normalize_gtin("123456789012345") == "3456789012345"
|
||||
) # Truncated to 13
|
||||
assert self.service._normalize_gtin("123456789012345") == "3456789012345" # Truncated to 13
|
||||
|
||||
def test_normalize_gtin_edge_cases(self):
|
||||
"""Test GTIN normalization edge cases."""
|
||||
# Test numeric inputs
|
||||
assert self.service.normalize_gtin(1234567890123) == "1234567890123"
|
||||
assert self.service.normalize_gtin(123) == "0000000000123"
|
||||
assert self.service._normalize_gtin(1234567890123) == "1234567890123"
|
||||
assert self.service._normalize_gtin(123) == "0000000000123"
|
||||
|
||||
# Test mixed valid/invalid characters
|
||||
assert (
|
||||
self.service.normalize_gtin("123-456-789-012") == "123456789012"
|
||||
) # Dashes removed
|
||||
assert (
|
||||
self.service.normalize_gtin("123 456 789 012") == "123456789012"
|
||||
) # Spaces removed
|
||||
assert (
|
||||
self.service.normalize_gtin("ABC123456789012DEF") == "123456789012"
|
||||
) # Letters removed
|
||||
assert self.service._normalize_gtin("123-456-789-012") == "123456789012" # Dashes removed
|
||||
assert self.service._normalize_gtin("123 456 789 012") == "123456789012" # Spaces removed
|
||||
assert self.service._normalize_gtin("ABC123456789012DEF") == "123456789012" # Letters removed
|
||||
|
||||
def test_set_stock_new_entry(self, db):
|
||||
"""Test setting stock for a new GTIN/location combination."""
|
||||
def test_set_stock_new_entry_success(self, db):
|
||||
"""Test setting stock for a new GTIN/location combination successfully."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
stock_data = StockCreate(
|
||||
gtin="1234567890123", location=f"WAREHOUSE_A_{unique_id}", quantity=100
|
||||
@@ -95,8 +82,8 @@ class TestStockService:
|
||||
assert result.location.upper() == f"WAREHOUSE_A_{unique_id}".upper()
|
||||
assert result.quantity == 100
|
||||
|
||||
def test_set_stock_existing_entry(self, db, test_stock):
|
||||
"""Test setting stock for an existing GTIN/location combination."""
|
||||
def test_set_stock_existing_entry_success(self, db, test_stock):
|
||||
"""Test setting stock for an existing GTIN/location combination successfully."""
|
||||
stock_data = StockCreate(
|
||||
gtin=test_stock.gtin,
|
||||
location=test_stock.location, # Use exact same location as test_stock
|
||||
@@ -106,21 +93,47 @@ class TestStockService:
|
||||
result = self.service.set_stock(db, stock_data)
|
||||
|
||||
assert result.gtin == test_stock.gtin
|
||||
# Fix: Handle case sensitivity properly - compare uppercase or use exact match
|
||||
assert result.location == test_stock.location
|
||||
assert result.quantity == 200 # Should replace the original quantity
|
||||
|
||||
def test_set_stock_invalid_gtin(self, db):
|
||||
"""Test setting stock with invalid GTIN."""
|
||||
def test_set_stock_invalid_gtin_validation_error(self, db):
|
||||
"""Test setting stock with invalid GTIN returns StockValidationException."""
|
||||
stock_data = StockCreate(
|
||||
gtin="invalid_gtin", location="WAREHOUSE_A", quantity=100
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid GTIN format"):
|
||||
with pytest.raises(StockValidationException) as exc_info:
|
||||
self.service.set_stock(db, stock_data)
|
||||
|
||||
def test_add_stock_new_entry(self, db):
|
||||
"""Test adding stock for a new GTIN/location combination."""
|
||||
assert exc_info.value.error_code == "STOCK_VALIDATION_FAILED"
|
||||
assert "Invalid GTIN format" in str(exc_info.value)
|
||||
assert exc_info.value.details.get("field") == "gtin"
|
||||
|
||||
def test_set_stock_invalid_quantity_error(self, db):
|
||||
"""Test setting stock with invalid quantity through service validation."""
|
||||
|
||||
# Test the service validation directly instead of going through Pydantic schema
|
||||
# This bypasses the Pydantic validation to test service layer validation
|
||||
|
||||
# Create a mock stock data object that bypasses Pydantic validation
|
||||
class MockStockData:
|
||||
def __init__(self, gtin, location, quantity):
|
||||
self.gtin = gtin
|
||||
self.location = location
|
||||
self.quantity = quantity
|
||||
|
||||
mock_stock_data = MockStockData("1234567890123", "WAREHOUSE_A", -10)
|
||||
|
||||
# Test the internal validation method directly
|
||||
with pytest.raises(InvalidQuantityException) as exc_info:
|
||||
self.service._validate_quantity(-10, allow_zero=True)
|
||||
|
||||
assert exc_info.value.error_code == "INVALID_QUANTITY"
|
||||
assert "Quantity cannot be negative" in str(exc_info.value)
|
||||
assert exc_info.value.details.get("quantity") == -10
|
||||
|
||||
def test_add_stock_new_entry_success(self, db):
|
||||
"""Test adding stock for a new GTIN/location combination successfully."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
stock_data = StockAdd(
|
||||
gtin="1234567890123", location=f"WAREHOUSE_B_{unique_id}", quantity=50
|
||||
@@ -132,8 +145,8 @@ class TestStockService:
|
||||
assert result.location.upper() == f"WAREHOUSE_B_{unique_id}".upper()
|
||||
assert result.quantity == 50
|
||||
|
||||
def test_add_stock_existing_entry(self, db, test_stock):
|
||||
"""Test adding stock to an existing GTIN/location combination."""
|
||||
def test_add_stock_existing_entry_success(self, db, test_stock):
|
||||
"""Test adding stock to an existing GTIN/location combination successfully."""
|
||||
original_quantity = test_stock.quantity
|
||||
stock_data = StockAdd(
|
||||
gtin=test_stock.gtin,
|
||||
@@ -147,19 +160,31 @@ class TestStockService:
|
||||
assert result.location == test_stock.location
|
||||
assert result.quantity == original_quantity + 25
|
||||
|
||||
def test_add_stock_invalid_gtin(self, db):
|
||||
"""Test adding stock with invalid GTIN."""
|
||||
def test_add_stock_invalid_gtin_validation_error(self, db):
|
||||
"""Test adding stock with invalid GTIN returns StockValidationException."""
|
||||
stock_data = StockAdd(gtin="invalid_gtin", location="WAREHOUSE_A", quantity=50)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid GTIN format"):
|
||||
with pytest.raises(StockValidationException) as exc_info:
|
||||
self.service.add_stock(db, stock_data)
|
||||
|
||||
assert exc_info.value.error_code == "STOCK_VALIDATION_FAILED"
|
||||
assert "Invalid GTIN format" in str(exc_info.value)
|
||||
|
||||
def test_add_stock_invalid_quantity_error(self, db):
|
||||
"""Test adding stock with invalid quantity through service validation."""
|
||||
# Test zero quantity which should fail for add_stock (doesn't allow zero)
|
||||
# This tests the service validation: allow_zero=False for add operations
|
||||
with pytest.raises(InvalidQuantityException) as exc_info:
|
||||
self.service._validate_quantity(0, allow_zero=False)
|
||||
|
||||
assert exc_info.value.error_code == "INVALID_QUANTITY"
|
||||
assert "Quantity must be positive" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_remove_stock_success(self, db, test_stock):
|
||||
"""Test removing stock successfully."""
|
||||
original_quantity = test_stock.quantity
|
||||
remove_quantity = min(
|
||||
10, original_quantity
|
||||
) # Ensure we don't remove more than available
|
||||
remove_quantity = min(10, original_quantity) # Ensure we don't remove more than available
|
||||
|
||||
stock_data = StockAdd(
|
||||
gtin=test_stock.gtin,
|
||||
@@ -173,39 +198,64 @@ class TestStockService:
|
||||
assert result.location == test_stock.location
|
||||
assert result.quantity == original_quantity - remove_quantity
|
||||
|
||||
def test_remove_stock_insufficient_stock(self, db, test_stock):
|
||||
"""Test removing more stock than available."""
|
||||
def test_remove_stock_insufficient_stock_error(self, db, test_stock):
|
||||
"""Test removing more stock than available returns InsufficientStockException."""
|
||||
stock_data = StockAdd(
|
||||
gtin=test_stock.gtin,
|
||||
location=test_stock.location, # Use exact same location as test_stock
|
||||
quantity=test_stock.quantity + 10, # More than available
|
||||
)
|
||||
|
||||
# Fix: Use more flexible regex pattern
|
||||
with pytest.raises(
|
||||
ValueError, match="Insufficient stock|Not enough stock|Cannot remove"
|
||||
):
|
||||
with pytest.raises(InsufficientStockException) as exc_info:
|
||||
self.service.remove_stock(db, stock_data)
|
||||
|
||||
def test_remove_stock_nonexistent_entry(self, db):
|
||||
"""Test removing stock from non-existent GTIN/location."""
|
||||
assert exc_info.value.error_code == "INSUFFICIENT_STOCK"
|
||||
assert exc_info.value.details["gtin"] == test_stock.gtin
|
||||
assert exc_info.value.details["location"] == test_stock.location
|
||||
assert exc_info.value.details["requested_quantity"] == test_stock.quantity + 10
|
||||
assert exc_info.value.details["available_quantity"] == test_stock.quantity
|
||||
|
||||
def test_remove_stock_nonexistent_entry_not_found(self, db):
|
||||
"""Test removing stock from non-existent GTIN/location returns StockNotFoundException."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
stock_data = StockAdd(
|
||||
gtin="9999999999999", location=f"NONEXISTENT_{unique_id}", quantity=10
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="No stock found|Stock not found"):
|
||||
with pytest.raises(StockNotFoundException) as exc_info:
|
||||
self.service.remove_stock(db, stock_data)
|
||||
|
||||
def test_remove_stock_invalid_gtin(self, db):
|
||||
"""Test removing stock with invalid GTIN."""
|
||||
assert exc_info.value.error_code == "STOCK_NOT_FOUND"
|
||||
assert "9999999999999" in str(exc_info.value)
|
||||
assert exc_info.value.details["resource_type"] == "Stock"
|
||||
|
||||
def test_remove_stock_invalid_gtin_validation_error(self, db):
|
||||
"""Test removing stock with invalid GTIN returns StockValidationException."""
|
||||
stock_data = StockAdd(gtin="invalid_gtin", location="WAREHOUSE_A", quantity=10)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid GTIN format"):
|
||||
with pytest.raises(StockValidationException) as exc_info:
|
||||
self.service.remove_stock(db, stock_data)
|
||||
|
||||
assert exc_info.value.error_code == "STOCK_VALIDATION_FAILED"
|
||||
assert "Invalid GTIN format" in str(exc_info.value)
|
||||
|
||||
def test_remove_stock_negative_result_error(self, db, test_stock):
|
||||
"""Test removing stock that would result in negative quantity returns NegativeStockException."""
|
||||
# This is handled by InsufficientStockException, but test the logic
|
||||
stock_data = StockAdd(
|
||||
gtin=test_stock.gtin,
|
||||
location=test_stock.location,
|
||||
quantity=test_stock.quantity + 1, # One more than available
|
||||
)
|
||||
|
||||
with pytest.raises(InsufficientStockException) as exc_info:
|
||||
self.service.remove_stock(db, stock_data)
|
||||
|
||||
# The service prevents negative stock through InsufficientStockException
|
||||
assert exc_info.value.error_code == "INSUFFICIENT_STOCK"
|
||||
|
||||
def test_get_stock_by_gtin_success(self, db, test_stock, test_product):
|
||||
"""Test getting stock summary by GTIN."""
|
||||
"""Test getting stock summary by GTIN successfully."""
|
||||
result = self.service.get_stock_by_gtin(db, test_stock.gtin)
|
||||
|
||||
assert result.gtin == test_stock.gtin
|
||||
@@ -215,19 +265,14 @@ class TestStockService:
|
||||
assert result.locations[0].quantity == test_stock.quantity
|
||||
assert result.product_title == test_product.title
|
||||
|
||||
def test_get_stock_by_gtin_multiple_locations(self, db, test_product):
|
||||
"""Test getting stock summary with multiple locations."""
|
||||
def test_get_stock_by_gtin_multiple_locations_success(self, db, test_product):
|
||||
"""Test getting stock summary with multiple locations successfully."""
|
||||
unique_gtin = test_product.gtin
|
||||
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
|
||||
# Create multiple stock entries for the same GTIN with unique locations
|
||||
stock1 = Stock(
|
||||
gtin=unique_gtin, location=f"WAREHOUSE_A_{unique_id}", quantity=50
|
||||
)
|
||||
stock2 = Stock(
|
||||
gtin=unique_gtin, location=f"WAREHOUSE_B_{unique_id}", quantity=30
|
||||
)
|
||||
stock1 = Stock(gtin=unique_gtin, location=f"WAREHOUSE_A_{unique_id}", quantity=50)
|
||||
stock2 = Stock(gtin=unique_gtin, location=f"WAREHOUSE_B_{unique_id}", quantity=30)
|
||||
|
||||
db.add(stock1)
|
||||
db.add(stock2)
|
||||
@@ -239,18 +284,25 @@ class TestStockService:
|
||||
assert result.total_quantity == 80
|
||||
assert len(result.locations) == 2
|
||||
|
||||
def test_get_stock_by_gtin_not_found(self, db):
|
||||
"""Test getting stock for non-existent GTIN."""
|
||||
with pytest.raises(ValueError, match="No stock found"):
|
||||
def test_get_stock_by_gtin_not_found_error(self, db):
|
||||
"""Test getting stock for non-existent GTIN returns StockNotFoundException."""
|
||||
with pytest.raises(StockNotFoundException) as exc_info:
|
||||
self.service.get_stock_by_gtin(db, "9999999999999")
|
||||
|
||||
def test_get_stock_by_gtin_invalid_gtin(self, db):
|
||||
"""Test getting stock with invalid GTIN."""
|
||||
with pytest.raises(ValueError, match="Invalid GTIN format"):
|
||||
assert exc_info.value.error_code == "STOCK_NOT_FOUND"
|
||||
assert "9999999999999" in str(exc_info.value)
|
||||
assert exc_info.value.details["resource_type"] == "Stock"
|
||||
|
||||
def test_get_stock_by_gtin_invalid_gtin_validation_error(self, db):
|
||||
"""Test getting stock with invalid GTIN returns StockValidationException."""
|
||||
with pytest.raises(StockValidationException) as exc_info:
|
||||
self.service.get_stock_by_gtin(db, "invalid_gtin")
|
||||
|
||||
assert exc_info.value.error_code == "STOCK_VALIDATION_FAILED"
|
||||
assert "Invalid GTIN format" in str(exc_info.value)
|
||||
|
||||
def test_get_total_stock_success(self, db, test_stock, test_product):
|
||||
"""Test getting total stock for a GTIN."""
|
||||
"""Test getting total stock for a GTIN successfully."""
|
||||
result = self.service.get_total_stock(db, test_stock.gtin)
|
||||
|
||||
assert result["gtin"] == test_stock.gtin
|
||||
@@ -258,43 +310,52 @@ class TestStockService:
|
||||
assert result["product_title"] == test_product.title
|
||||
assert result["locations_count"] == 1
|
||||
|
||||
def test_get_total_stock_invalid_gtin(self, db):
|
||||
"""Test getting total stock with invalid GTIN."""
|
||||
with pytest.raises(ValueError, match="Invalid GTIN format"):
|
||||
def test_get_total_stock_invalid_gtin_validation_error(self, db):
|
||||
"""Test getting total stock with invalid GTIN returns StockValidationException."""
|
||||
with pytest.raises(StockValidationException) as exc_info:
|
||||
self.service.get_total_stock(db, "invalid_gtin")
|
||||
|
||||
def test_get_all_stock_no_filters(self, db, test_stock):
|
||||
"""Test getting all stock without filters."""
|
||||
assert exc_info.value.error_code == "STOCK_VALIDATION_FAILED"
|
||||
assert "Invalid GTIN format" in str(exc_info.value)
|
||||
|
||||
def test_get_total_stock_not_found_error(self, db):
|
||||
"""Test getting total stock for non-existent GTIN returns StockNotFoundException."""
|
||||
with pytest.raises(StockNotFoundException) as exc_info:
|
||||
self.service.get_total_stock(db, "9999999999999")
|
||||
|
||||
assert exc_info.value.error_code == "STOCK_NOT_FOUND"
|
||||
|
||||
def test_get_all_stock_no_filters_success(self, db, test_stock):
|
||||
"""Test getting all stock without filters successfully."""
|
||||
result = self.service.get_all_stock(db)
|
||||
|
||||
assert len(result) >= 1
|
||||
assert any(stock.gtin == test_stock.gtin for stock in result)
|
||||
|
||||
def test_get_all_stock_with_location_filter(self, db, test_stock):
|
||||
"""Test getting all stock with location filter."""
|
||||
def test_get_all_stock_with_location_filter_success(self, db, test_stock):
|
||||
"""Test getting all stock with location filter successfully."""
|
||||
result = self.service.get_all_stock(db, location=test_stock.location)
|
||||
|
||||
assert len(result) >= 1
|
||||
# Fix: Handle case sensitivity in comparison
|
||||
assert all(
|
||||
stock.location.upper() == test_stock.location.upper() for stock in result
|
||||
)
|
||||
# Check that all returned stocks match the filter (case insensitive)
|
||||
for stock in result:
|
||||
assert test_stock.location.upper() in stock.location.upper()
|
||||
|
||||
def test_get_all_stock_with_gtin_filter(self, db, test_stock):
|
||||
"""Test getting all stock with GTIN filter."""
|
||||
def test_get_all_stock_with_gtin_filter_success(self, db, test_stock):
|
||||
"""Test getting all stock with GTIN filter successfully."""
|
||||
result = self.service.get_all_stock(db, gtin=test_stock.gtin)
|
||||
|
||||
assert len(result) >= 1
|
||||
assert all(stock.gtin == test_stock.gtin for stock in result)
|
||||
|
||||
def test_get_all_stock_with_pagination(self, db):
|
||||
"""Test getting all stock with pagination."""
|
||||
def test_get_all_stock_with_pagination_success(self, db):
|
||||
"""Test getting all stock with pagination successfully."""
|
||||
unique_prefix = str(uuid.uuid4())[:8]
|
||||
|
||||
# Create multiple stock entries with unique GTINs and locations
|
||||
for i in range(5):
|
||||
stock = Stock(
|
||||
gtin=f"1234567890{i:03d}", # Creates valid 13-digit GTINs: 1234567890000, 1234567890001, etc.
|
||||
gtin=f"1234567890{i:03d}", # Creates valid 13-digit GTINs
|
||||
location=f"WAREHOUSE_{unique_prefix}_{i}",
|
||||
quantity=10,
|
||||
)
|
||||
@@ -303,12 +364,10 @@ class TestStockService:
|
||||
|
||||
result = self.service.get_all_stock(db, skip=2, limit=2)
|
||||
|
||||
assert (
|
||||
len(result) <= 2
|
||||
) # Should be at most 2, might be less if other records exist
|
||||
assert len(result) <= 2 # Should be at most 2, might be less if other records exist
|
||||
|
||||
def test_update_stock_success(self, db, test_stock):
|
||||
"""Test updating stock quantity."""
|
||||
"""Test updating stock quantity successfully."""
|
||||
stock_update = StockUpdate(quantity=150)
|
||||
|
||||
result = self.service.update_stock(db, test_stock.id, stock_update)
|
||||
@@ -316,15 +375,28 @@ class TestStockService:
|
||||
assert result.id == test_stock.id
|
||||
assert result.quantity == 150
|
||||
|
||||
def test_update_stock_not_found(self, db):
|
||||
"""Test updating non-existent stock entry."""
|
||||
def test_update_stock_not_found_error(self, db):
|
||||
"""Test updating non-existent stock entry returns StockNotFoundException."""
|
||||
stock_update = StockUpdate(quantity=150)
|
||||
|
||||
with pytest.raises(ValueError, match="Stock entry not found"):
|
||||
with pytest.raises(StockNotFoundException) as exc_info:
|
||||
self.service.update_stock(db, 99999, stock_update)
|
||||
|
||||
assert exc_info.value.error_code == "STOCK_NOT_FOUND"
|
||||
assert "99999" in str(exc_info.value)
|
||||
|
||||
def test_update_stock_invalid_quantity_error(self, db, test_stock):
|
||||
"""Test updating stock with invalid quantity returns InvalidQuantityException."""
|
||||
stock_update = StockUpdate(quantity=-10)
|
||||
|
||||
with pytest.raises(InvalidQuantityException) as exc_info:
|
||||
self.service.update_stock(db, test_stock.id, stock_update)
|
||||
|
||||
assert exc_info.value.error_code == "INVALID_QUANTITY"
|
||||
assert "Quantity cannot be negative" in str(exc_info.value)
|
||||
|
||||
def test_delete_stock_success(self, db, test_stock):
|
||||
"""Test deleting stock entry."""
|
||||
"""Test deleting stock entry successfully."""
|
||||
stock_id = test_stock.id
|
||||
|
||||
result = self.service.delete_stock(db, stock_id)
|
||||
@@ -335,24 +407,85 @@ class TestStockService:
|
||||
deleted_stock = db.query(Stock).filter(Stock.id == stock_id).first()
|
||||
assert deleted_stock is None
|
||||
|
||||
def test_delete_stock_not_found(self, db):
|
||||
"""Test deleting non-existent stock entry."""
|
||||
with pytest.raises(ValueError, match="Stock entry not found"):
|
||||
def test_delete_stock_not_found_error(self, db):
|
||||
"""Test deleting non-existent stock entry returns StockNotFoundException."""
|
||||
with pytest.raises(StockNotFoundException) as exc_info:
|
||||
self.service.delete_stock(db, 99999)
|
||||
|
||||
def test_get_stock_by_id_success(self, db, test_stock):
|
||||
"""Test getting stock entry by ID."""
|
||||
result = self.service.get_stock_by_id(db, test_stock.id)
|
||||
assert exc_info.value.error_code == "STOCK_NOT_FOUND"
|
||||
assert "99999" in str(exc_info.value)
|
||||
|
||||
assert result is not None
|
||||
assert result.id == test_stock.id
|
||||
assert result.gtin == test_stock.gtin
|
||||
def test_get_low_stock_items_success(self, db, test_stock, test_product):
|
||||
"""Test getting low stock items successfully."""
|
||||
# Set stock to a low value
|
||||
test_stock.quantity = 5
|
||||
db.commit()
|
||||
|
||||
def test_get_stock_by_id_not_found(self, db):
|
||||
"""Test getting non-existent stock entry by ID."""
|
||||
result = self.service.get_stock_by_id(db, 99999)
|
||||
result = self.service.get_low_stock_items(db, threshold=10)
|
||||
|
||||
assert result is None
|
||||
assert len(result) >= 1
|
||||
low_stock_item = next((item for item in result if item["gtin"] == test_stock.gtin), None)
|
||||
assert low_stock_item is not None
|
||||
assert low_stock_item["current_quantity"] == 5
|
||||
assert low_stock_item["location"] == test_stock.location
|
||||
assert low_stock_item["product_title"] == test_product.title
|
||||
|
||||
def test_get_low_stock_items_invalid_threshold_error(self, db):
|
||||
"""Test getting low stock items with invalid threshold returns InvalidQuantityException."""
|
||||
with pytest.raises(InvalidQuantityException) as exc_info:
|
||||
self.service.get_low_stock_items(db, threshold=-5)
|
||||
|
||||
assert exc_info.value.error_code == "INVALID_QUANTITY"
|
||||
assert "Threshold must be non-negative" in str(exc_info.value)
|
||||
|
||||
def test_get_stock_summary_by_location_success(self, db, test_stock):
|
||||
"""Test getting stock summary by location successfully."""
|
||||
result = self.service.get_stock_summary_by_location(db, test_stock.location)
|
||||
|
||||
assert result["location"] == test_stock.location.upper() # Service normalizes to uppercase
|
||||
assert result["total_items"] >= 1
|
||||
assert result["total_quantity"] >= test_stock.quantity
|
||||
assert result["unique_gtins"] >= 1
|
||||
|
||||
def test_get_stock_summary_by_location_empty_result(self, db):
|
||||
"""Test getting stock summary for location with no stock."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
result = self.service.get_stock_summary_by_location(db, f"EMPTY_LOCATION_{unique_id}")
|
||||
|
||||
assert result["total_items"] == 0
|
||||
assert result["total_quantity"] == 0
|
||||
assert result["unique_gtins"] == 0
|
||||
|
||||
def test_validate_quantity_edge_cases(self, db):
|
||||
"""Test quantity validation with edge cases."""
|
||||
# Test zero quantity with allow_zero=True (should succeed)
|
||||
stock_data = StockCreate(gtin="1234567890123", location="WAREHOUSE_A", quantity=0)
|
||||
result = self.service.set_stock(db, stock_data)
|
||||
assert result.quantity == 0
|
||||
|
||||
# Test zero quantity with add_stock (should fail - doesn't allow zero)
|
||||
stock_data_add = StockAdd(gtin="1234567890123", location="WAREHOUSE_B", quantity=0)
|
||||
with pytest.raises(InvalidQuantityException):
|
||||
self.service.add_stock(db, stock_data_add)
|
||||
|
||||
def test_exception_structure_consistency(self, db):
|
||||
"""Test that all exceptions follow the consistent LetzShopException structure."""
|
||||
# Test with a known error case
|
||||
with pytest.raises(StockNotFoundException) as exc_info:
|
||||
self.service.get_stock_by_gtin(db, "9999999999999")
|
||||
|
||||
exception = exc_info.value
|
||||
|
||||
# Verify exception structure matches LetzShopException.to_dict()
|
||||
assert hasattr(exception, 'error_code')
|
||||
assert hasattr(exception, 'message')
|
||||
assert hasattr(exception, 'status_code')
|
||||
assert hasattr(exception, 'details')
|
||||
|
||||
assert isinstance(exception.error_code, str)
|
||||
assert isinstance(exception.message, str)
|
||||
assert isinstance(exception.status_code, int)
|
||||
assert isinstance(exception.details, dict)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
Reference in New Issue
Block a user