Files
orion/tests/integration/api/v1/test_stock_endpoints.py

456 lines
17 KiB
Python

# tests/integration/api/v1/test_stock_endpoints.py
import pytest
from models.database.stock import Stock
@pytest.mark.integration
@pytest.mark.api
@pytest.mark.stock
class TestStockAPI:
def test_set_stock_new_success(self, client, auth_headers):
"""Test setting stock for new GTIN successfully"""
stock_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": 100,
}
response = client.post("/api/v1/stock", headers=auth_headers, json=stock_data)
assert response.status_code == 200
data = response.json()
assert data["gtin"] == "1234567890123"
assert data["location"] == "WAREHOUSE_A"
assert data["quantity"] == 100
def test_set_stock_existing_success(self, client, auth_headers, db):
"""Test updating existing stock successfully"""
# Create initial stock
stock = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
db.add(stock)
db.commit()
stock_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": 75,
}
response = client.post("/api/v1/stock", headers=auth_headers, json=stock_data)
assert response.status_code == 200
data = response.json()
assert data["quantity"] == 75 # Should be replaced, not added
def test_set_stock_invalid_gtin_validation_error(self, client, auth_headers):
"""Test setting stock with invalid GTIN returns ValidationException"""
stock_data = {
"gtin": "", # Empty GTIN
"location": "WAREHOUSE_A",
"quantity": 100,
}
response = client.post("/api/v1/stock", headers=auth_headers, json=stock_data)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "STOCK_VALIDATION_FAILED"
assert data["status_code"] == 422
assert "GTIN is required" in data["message"]
def test_set_stock_invalid_quantity_validation_error(self, client, auth_headers):
"""Test setting stock with invalid quantity returns InvalidQuantityException"""
stock_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": -10, # Negative quantity
}
response = client.post("/api/v1/stock", headers=auth_headers, json=stock_data)
assert response.status_code in [400, 422]
data = response.json()
assert data["error_code"] in ["INVALID_QUANTITY", "VALIDATION_ERROR"]
if data["error_code"] == "INVALID_QUANTITY":
assert data["status_code"] == 422
assert data["details"]["field"] == "quantity"
def test_add_stock_success(self, client, auth_headers, db):
"""Test adding to existing stock successfully"""
# Create initial stock
stock = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
db.add(stock)
db.commit()
stock_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": 25,
}
response = client.post(
"/api/v1/stock/add", headers=auth_headers, json=stock_data
)
assert response.status_code == 200
data = response.json()
assert data["quantity"] == 75 # 50 + 25
def test_add_stock_creates_new_if_not_exists(self, client, auth_headers):
"""Test adding to nonexistent stock creates new stock entry"""
stock_data = {
"gtin": "9999999999999",
"location": "WAREHOUSE_A",
"quantity": 25,
}
response = client.post(
"/api/v1/stock/add", headers=auth_headers, json=stock_data
)
# Your service creates new stock if it doesn't exist (upsert behavior)
assert response.status_code == 200
data = response.json()
assert data["gtin"] == "9999999999999"
assert data["location"] == "WAREHOUSE_A"
assert data["quantity"] == 25
def test_remove_stock_success(self, client, auth_headers, db):
"""Test removing from existing stock successfully"""
# Create initial stock
stock = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
db.add(stock)
db.commit()
stock_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": 15,
}
response = client.post(
"/api/v1/stock/remove", headers=auth_headers, json=stock_data
)
assert response.status_code == 200
data = response.json()
assert data["quantity"] == 35 # 50 - 15
def test_remove_stock_insufficient_returns_business_logic_error(self, client, auth_headers, db):
"""Test removing more stock than available returns InsufficientStockException"""
# Create initial stock
stock = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=10)
db.add(stock)
db.commit()
stock_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": 20,
}
response = client.post(
"/api/v1/stock/remove", headers=auth_headers, json=stock_data
)
assert response.status_code == 400
data = response.json()
assert data["error_code"] == "INSUFFICIENT_STOCK"
assert data["status_code"] == 400
assert "Insufficient stock" in data["message"]
assert data["details"]["gtin"] == "1234567890123"
assert data["details"]["location"] == "WAREHOUSE_A"
assert data["details"]["requested_quantity"] == 20
assert data["details"]["available_quantity"] == 10
def test_remove_stock_not_found(self, client, auth_headers):
"""Test removing from nonexistent stock returns StockNotFoundException"""
stock_data = {
"gtin": "9999999999999",
"location": "WAREHOUSE_A",
"quantity": 15,
}
response = client.post(
"/api/v1/stock/remove", headers=auth_headers, json=stock_data
)
# This should actually return 404 since you can't remove from non-existent stock
# If it returns 200, your service might create stock with negative quantity
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "STOCK_NOT_FOUND"
assert data["status_code"] == 404
def test_negative_stock_not_allowed_business_logic_error(self, client, auth_headers, db):
"""Test operations resulting in negative stock returns NegativeStockException"""
# Create initial stock
stock = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=5)
db.add(stock)
db.commit()
stock_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": 10,
}
response = client.post(
"/api/v1/stock/remove", headers=auth_headers, json=stock_data
)
assert response.status_code == 400
data = response.json()
# This might be caught as INSUFFICIENT_STOCK or NEGATIVE_STOCK_NOT_ALLOWED
assert data["error_code"] in ["INSUFFICIENT_STOCK", "NEGATIVE_STOCK_NOT_ALLOWED"]
assert data["status_code"] == 400
def test_get_stock_by_gtin_success(self, client, auth_headers, db):
"""Test getting stock summary for GTIN successfully"""
# Create stock in multiple locations
stock1 = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
stock2 = Stock(gtin="1234567890123", location="WAREHOUSE_B", quantity=25)
db.add_all([stock1, stock2])
db.commit()
response = client.get("/api/v1/stock/1234567890123", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert data["gtin"] == "1234567890123"
assert data["total_quantity"] == 75
assert len(data["locations"]) == 2
def test_get_stock_by_gtin_not_found(self, client, auth_headers):
"""Test getting stock for nonexistent GTIN returns StockNotFoundException"""
response = client.get("/api/v1/stock/9999999999999", headers=auth_headers)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "STOCK_NOT_FOUND"
assert data["status_code"] == 404
assert "9999999999999" in data["message"]
assert data["details"]["resource_type"] == "Stock"
assert data["details"]["identifier"] == "9999999999999"
def test_get_total_stock_success(self, client, auth_headers, db):
"""Test getting total stock for GTIN successfully"""
# Create stock in multiple locations
stock1 = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
stock2 = Stock(gtin="1234567890123", location="WAREHOUSE_B", quantity=25)
db.add_all([stock1, stock2])
db.commit()
response = client.get("/api/v1/stock/1234567890123/total", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert data["gtin"] == "1234567890123"
assert data["total_quantity"] == 75
assert data["locations_count"] == 2
def test_get_total_stock_not_found(self, client, auth_headers):
"""Test getting total stock for nonexistent GTIN returns StockNotFoundException"""
response = client.get("/api/v1/stock/9999999999999/total", headers=auth_headers)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "STOCK_NOT_FOUND"
assert data["status_code"] == 404
def test_get_all_stock_success(self, client, auth_headers, db):
"""Test getting all stock entries successfully"""
# Create some stock entries
stock1 = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
stock2 = Stock(gtin="9876543210987", location="WAREHOUSE_B", quantity=25)
db.add_all([stock1, stock2])
db.commit()
response = client.get("/api/v1/stock", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert len(data) >= 2
def test_get_all_stock_with_filters(self, client, auth_headers, db):
"""Test getting stock entries with filtering"""
# Create stock entries
stock1 = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
stock2 = Stock(gtin="9876543210987", location="WAREHOUSE_B", quantity=25)
db.add_all([stock1, stock2])
db.commit()
# Filter by location
response = client.get("/api/v1/stock?location=WAREHOUSE_A", headers=auth_headers)
assert response.status_code == 200
data = response.json()
for stock in data:
assert stock["location"] == "WAREHOUSE_A"
# Filter by GTIN
response = client.get("/api/v1/stock?gtin=1234567890123", headers=auth_headers)
assert response.status_code == 200
data = response.json()
for stock in data:
assert stock["gtin"] == "1234567890123"
def test_update_stock_success(self, client, auth_headers, db):
"""Test updating stock quantity successfully"""
# Create initial stock
stock = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
db.add(stock)
db.commit()
db.refresh(stock)
update_data = {"quantity": 75}
response = client.put(
f"/api/v1/stock/{stock.id}",
headers=auth_headers,
json=update_data,
)
assert response.status_code == 200
data = response.json()
assert data["quantity"] == 75
def test_update_stock_not_found(self, client, auth_headers):
"""Test updating nonexistent stock returns StockNotFoundException"""
update_data = {"quantity": 75}
response = client.put(
"/api/v1/stock/99999",
headers=auth_headers,
json=update_data,
)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "STOCK_NOT_FOUND"
assert data["status_code"] == 404
def test_update_stock_invalid_quantity(self, client, auth_headers, db):
"""Test updating stock with invalid quantity returns ValidationException"""
# Create initial stock
stock = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
db.add(stock)
db.commit()
db.refresh(stock)
update_data = {"quantity": -10} # Negative quantity
response = client.put(
f"/api/v1/stock/{stock.id}",
headers=auth_headers,
json=update_data,
)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "INVALID_QUANTITY"
assert data["status_code"] == 422
assert "Quantity cannot be negative" in data["message"]
assert data["details"]["field"] == "quantity"
def test_delete_stock_success(self, client, auth_headers, db):
"""Test deleting stock entry successfully"""
# Create initial stock
stock = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
db.add(stock)
db.commit()
db.refresh(stock)
response = client.delete(
f"/api/v1/stock/{stock.id}",
headers=auth_headers,
)
assert response.status_code == 200
assert "deleted successfully" in response.json()["message"]
def test_delete_stock_not_found(self, client, auth_headers):
"""Test deleting nonexistent stock returns StockNotFoundException"""
response = client.delete(
"/api/v1/stock/99999",
headers=auth_headers,
)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "STOCK_NOT_FOUND"
assert data["status_code"] == 404
def test_location_not_found_error(self, client, auth_headers):
"""Test operations on nonexistent location returns LocationNotFoundException (if implemented)"""
stock_data = {
"gtin": "1234567890123",
"location": "NONEXISTENT_LOCATION",
"quantity": 100,
}
response = client.post("/api/v1/stock", headers=auth_headers, json=stock_data)
# This depends on whether your service validates locations
if response.status_code == 404:
data = response.json()
assert data["error_code"] == "LOCATION_NOT_FOUND"
assert data["status_code"] == 404
def test_invalid_stock_operation_error(self, client, auth_headers):
"""Test invalid stock operations return InvalidStockOperationException"""
# This would test business logic validation
# The exact scenario depends on your business rules
pass # Implementation depends on specific business rules
def test_get_stock_without_auth_returns_invalid_token(self, client):
"""Test that stock endpoints require authentication returns InvalidTokenException"""
response = client.get("/api/v1/stock")
assert response.status_code == 401
data = response.json()
assert data["error_code"] == "INVALID_TOKEN"
assert data["status_code"] == 401
def test_pagination_validation_errors(self, client, auth_headers):
"""Test pagination parameter validation"""
# Test negative skip
response = client.get("/api/v1/stock?skip=-1", headers=auth_headers)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "VALIDATION_ERROR"
# Test zero limit
response = client.get("/api/v1/stock?limit=0", headers=auth_headers)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "VALIDATION_ERROR"
# Test excessive limit
response = client.get("/api/v1/stock?limit=10000", headers=auth_headers)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "VALIDATION_ERROR"
def test_exception_structure_consistency(self, client, auth_headers):
"""Test that all stock exceptions follow the consistent LetzShopException structure"""
# Test with a known error case
response = client.get("/api/v1/stock/9999999999999", headers=auth_headers)
assert response.status_code == 404
data = response.json()
# Verify exception structure matches LetzShopException.to_dict()
required_fields = ["error_code", "message", "status_code"]
for field in required_fields:
assert field in data, f"Missing required field: {field}"
assert isinstance(data["error_code"], str)
assert isinstance(data["message"], str)
assert isinstance(data["status_code"], int)
# Details field should be present for domain-specific exceptions
if "details" in data:
assert isinstance(data["details"], dict)