- Replace black, isort, and flake8 with Ruff (all-in-one linter and formatter) - Add comprehensive pyproject.toml configuration - Simplify Makefile code quality targets - Configure exclusions for venv/.venv in pyproject.toml - Auto-fix 1,359 linting issues across codebase Benefits: - Much faster builds (Ruff is written in Rust) - Single tool replaces multiple tools - More comprehensive rule set (UP, B, C4, SIM, PIE, RET, Q) - All configuration centralized in pyproject.toml - Better import sorting and formatting consistency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
418 lines
16 KiB
Python
418 lines
16 KiB
Python
# tests/integration/api/v1/test_vendor_endpoints.py
|
|
import pytest
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.api
|
|
@pytest.mark.vendors
|
|
class TestVendorsAPI:
|
|
def test_create_vendor_success(self, client, auth_headers):
|
|
"""Test creating a new vendor successfully"""
|
|
vendor_data = {
|
|
"vendor_code": "NEWVENDOR001",
|
|
"name": "New Vendor",
|
|
"description": "A new test vendor ",
|
|
}
|
|
|
|
response = client.post("/api/v1/vendor", headers=auth_headers, json=vendor_data)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["vendor_code"] == "NEWVENDOR001"
|
|
assert data["name"] == "New Vendor"
|
|
assert data["is_active"] is True
|
|
|
|
def test_create_vendor_duplicate_code_returns_conflict(
|
|
self, client, auth_headers, test_vendor
|
|
):
|
|
"""Test creating vendor with duplicate code returns VendorAlreadyExistsException"""
|
|
vendor_data = {
|
|
"vendor_code": test_vendor.vendor_code,
|
|
"name": "Different Name",
|
|
"description": "Different description",
|
|
}
|
|
|
|
response = client.post("/api/v1/vendor", headers=auth_headers, json=vendor_data)
|
|
|
|
assert response.status_code == 409
|
|
data = response.json()
|
|
assert data["error_code"] == "VENDOR_ALREADY_EXISTS"
|
|
assert data["status_code"] == 409
|
|
assert test_vendor.vendor_code in data["message"]
|
|
assert data["details"]["vendor_code"] == test_vendor.vendor_code
|
|
|
|
def test_create_vendor_missing_vendor_code_validation_error(
|
|
self, client, auth_headers
|
|
):
|
|
"""Test creating vendor without vendor_code returns ValidationException"""
|
|
vendor_data = {
|
|
"name": "Vendor without Code",
|
|
"description": "Missing vendor code",
|
|
}
|
|
|
|
response = client.post("/api/v1/vendor", headers=auth_headers, json=vendor_data)
|
|
|
|
assert response.status_code == 422
|
|
data = response.json()
|
|
assert data["error_code"] == "VALIDATION_ERROR"
|
|
assert data["status_code"] == 422
|
|
assert "Request validation failed" in data["message"]
|
|
assert "validation_errors" in data["details"]
|
|
|
|
def test_create_vendor_empty_vendor_name_validation_error(
|
|
self, client, auth_headers
|
|
):
|
|
"""Test creating vendor with empty name returns VendorValidationException"""
|
|
vendor_data = {
|
|
"vendor_code": "EMPTYNAME",
|
|
"name": "", # Empty vendor name
|
|
"description": "Vendor with empty name",
|
|
}
|
|
|
|
response = client.post("/api/v1/vendor", headers=auth_headers, json=vendor_data)
|
|
|
|
assert response.status_code == 422
|
|
data = response.json()
|
|
assert data["error_code"] == "INVALID_VENDOR_DATA"
|
|
assert data["status_code"] == 422
|
|
assert "Vendor name is required" in data["message"]
|
|
assert data["details"]["field"] == "name"
|
|
|
|
def test_create_vendor_max_vendors_reached_business_logic_error(
|
|
self, client, auth_headers, db, test_user
|
|
):
|
|
"""Test creating vendor when max vendors reached returns MaxVendorsReachedException"""
|
|
# This test would require creating the maximum allowed vendors first
|
|
# The exact implementation depends on your business rules
|
|
|
|
# For now, we'll test the structure of what the error should look like
|
|
# In a real scenario, you'd create max_vendors number of vendors first
|
|
|
|
# Assuming max vendors is enforced at service level
|
|
# This test validates the expected response structure
|
|
# Implementation depends on your max_vendors business logic
|
|
|
|
def test_get_vendors_success(self, client, auth_headers, test_vendor):
|
|
"""Test getting vendors list successfully"""
|
|
response = client.get("/api/v1/vendor", headers=auth_headers)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total"] >= 1
|
|
assert len(data["vendors"]) >= 1
|
|
|
|
# Find our test vendor
|
|
test_vendor_found = any(
|
|
s["vendor_code"] == test_vendor.vendor_code for s in data["vendors"]
|
|
)
|
|
assert test_vendor_found
|
|
|
|
def test_get_vendors_with_filters(self, client, auth_headers, test_vendor):
|
|
"""Test getting vendors with filtering options"""
|
|
# Test active_only filter
|
|
response = client.get("/api/v1/vendor?active_only=true", headers=auth_headers)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
for vendor in data["vendors"]:
|
|
assert vendor["is_active"] is True
|
|
|
|
# Test verified_only filter
|
|
response = client.get("/api/v1/vendor?verified_only=true", headers=auth_headers)
|
|
assert response.status_code == 200
|
|
# Response should only contain verified vendors
|
|
|
|
def test_get_vendor_by_code_success(self, client, auth_headers, test_vendor):
|
|
"""Test getting specific vendor successfully"""
|
|
response = client.get(
|
|
f"/api/v1/vendor/{test_vendor.vendor_code}", headers=auth_headers
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["vendor_code"] == test_vendor.vendor_code
|
|
assert data["name"] == test_vendor.name
|
|
|
|
def test_get_vendor_by_code_not_found(self, client, auth_headers):
|
|
"""Test getting nonexistent vendor returns VendorNotFoundException"""
|
|
response = client.get("/api/v1/vendor/NONEXISTENT", headers=auth_headers)
|
|
|
|
assert response.status_code == 404
|
|
data = response.json()
|
|
assert data["error_code"] == "VENDOR_NOT_FOUND"
|
|
assert data["status_code"] == 404
|
|
assert "NONEXISTENT" in data["message"]
|
|
assert data["details"]["resource_type"] == "Vendor"
|
|
assert data["details"]["identifier"] == "NONEXISTENT"
|
|
|
|
def test_get_vendor_unauthorized_access(
|
|
self, client, auth_headers, test_vendor, other_user, db
|
|
):
|
|
"""Test accessing vendor owned by another user returns UnauthorizedVendorAccessException"""
|
|
# Change vendor owner to other user AND make it unverified/inactive
|
|
# so that non-owner users cannot access it
|
|
test_vendor.owner_user_id = other_user.id
|
|
test_vendor.is_verified = False # Make it not publicly accessible
|
|
db.commit()
|
|
|
|
response = client.get(
|
|
f"/api/v1/vendor/{test_vendor.vendor_code}", headers=auth_headers
|
|
)
|
|
|
|
assert response.status_code == 403
|
|
data = response.json()
|
|
assert data["error_code"] == "UNAUTHORIZED_VENDOR_ACCESS"
|
|
assert data["status_code"] == 403
|
|
assert test_vendor.vendor_code in data["message"]
|
|
assert data["details"]["vendor_code"] == test_vendor.vendor_code
|
|
|
|
def test_get_vendor_unauthorized_access_with_inactive_vendor(
|
|
self, client, auth_headers, inactive_vendor
|
|
):
|
|
"""Test accessing inactive vendor owned by another user returns UnauthorizedVendorAccessException"""
|
|
# inactive_vendor fixture already creates an unverified, inactive vendor owned by other_user
|
|
response = client.get(
|
|
f"/api/v1/vendor/{inactive_vendor.vendor_code}", headers=auth_headers
|
|
)
|
|
|
|
assert response.status_code == 403
|
|
data = response.json()
|
|
assert data["error_code"] == "UNAUTHORIZED_VENDOR_ACCESS"
|
|
assert data["status_code"] == 403
|
|
assert inactive_vendor.vendor_code in data["message"]
|
|
assert data["details"]["vendor_code"] == inactive_vendor.vendor_code
|
|
|
|
def test_get_vendor_public_access_allowed(
|
|
self, client, auth_headers, verified_vendor
|
|
):
|
|
"""Test accessing verified vendor owned by another user is allowed (public access)"""
|
|
# verified_vendor fixture creates a verified, active vendor owned by other_user
|
|
# This should allow public access per your business logic
|
|
response = client.get(
|
|
f"/api/v1/vendor/{verified_vendor.vendor_code}", headers=auth_headers
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["vendor_code"] == verified_vendor.vendor_code
|
|
assert data["name"] == verified_vendor.name
|
|
|
|
def test_add_product_to_vendor_success(
|
|
self, client, auth_headers, test_vendor, unique_product
|
|
):
|
|
"""Test adding product to vendor successfully"""
|
|
product_data = {
|
|
"marketplace_product_id": unique_product.marketplace_product_id, # Use string marketplace_product_id, not database id
|
|
"price": 29.99,
|
|
"is_active": True,
|
|
"is_featured": False,
|
|
}
|
|
|
|
response = client.post(
|
|
f"/api/v1/vendor/{test_vendor.vendor_code}/products",
|
|
headers=auth_headers,
|
|
json=product_data,
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
# The response structure contains nested product data
|
|
assert data["vendor_id"] == test_vendor.id
|
|
assert data["price"] == 29.99
|
|
assert data["is_active"] is True
|
|
assert data["is_featured"] is False
|
|
|
|
# MarketplaceProduct details are nested in the 'marketplace_product' field
|
|
assert "marketplace_product" in data
|
|
assert (
|
|
data["marketplace_product"]["marketplace_product_id"]
|
|
== unique_product.marketplace_product_id
|
|
)
|
|
assert data["marketplace_product"]["id"] == unique_product.id
|
|
|
|
def test_add_product_to_vendor_already_exists_conflict(
|
|
self, client, auth_headers, test_vendor, test_product
|
|
):
|
|
"""Test adding product that already exists in vendor returns ProductAlreadyExistsException"""
|
|
# test_product fixture already creates a relationship, get the marketplace_product_id string
|
|
existing_product = test_product.marketplace_product
|
|
|
|
product_data = {
|
|
"marketplace_product_id": existing_product.marketplace_product_id, # Use string marketplace_product_id
|
|
"price": 29.99,
|
|
}
|
|
|
|
response = client.post(
|
|
f"/api/v1/vendor/{test_vendor.vendor_code}/products",
|
|
headers=auth_headers,
|
|
json=product_data,
|
|
)
|
|
|
|
assert response.status_code == 409
|
|
data = response.json()
|
|
assert data["error_code"] == "PRODUCT_ALREADY_EXISTS"
|
|
assert data["status_code"] == 409
|
|
assert test_vendor.vendor_code in data["message"]
|
|
assert existing_product.marketplace_product_id in data["message"]
|
|
|
|
def test_add_nonexistent_product_to_vendor_not_found(
|
|
self, client, auth_headers, test_vendor
|
|
):
|
|
"""Test adding nonexistent product to vendor returns MarketplaceProductNotFoundException"""
|
|
product_data = {
|
|
"marketplace_product_id": "NONEXISTENT_PRODUCT", # Use string marketplace_product_id that doesn't exist
|
|
"price": 29.99,
|
|
}
|
|
|
|
response = client.post(
|
|
f"/api/v1/vendor/{test_vendor.vendor_code}/products",
|
|
headers=auth_headers,
|
|
json=product_data,
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
data = response.json()
|
|
assert data["error_code"] == "PRODUCT_NOT_FOUND"
|
|
assert data["status_code"] == 404
|
|
assert "NONEXISTENT_PRODUCT" in data["message"]
|
|
|
|
def test_get_products_success(
|
|
self, client, auth_headers, test_vendor, test_product
|
|
):
|
|
"""Test getting vendor products successfully"""
|
|
response = client.get(
|
|
f"/api/v1/vendor/{test_vendor.vendor_code}/products", headers=auth_headers
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total"] >= 1
|
|
assert len(data["products"]) >= 1
|
|
assert "vendor" in data
|
|
assert data["vendor"]["vendor_code"] == test_vendor.vendor_code
|
|
|
|
def test_get_products_with_filters(self, client, auth_headers, test_vendor):
|
|
"""Test getting vendor products with filtering"""
|
|
# Test active_only filter
|
|
response = client.get(
|
|
f"/api/v1/vendor/{test_vendor.vendor_code}/products?active_only=true",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
# Test featured_only filter
|
|
response = client.get(
|
|
f"/api/v1/vendor/{test_vendor.vendor_code}/products?featured_only=true",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
def test_get_products_from_nonexistent_vendor_not_found(self, client, auth_headers):
|
|
"""Test getting products from nonexistent vendor returns VendorNotFoundException"""
|
|
response = client.get(
|
|
"/api/v1/vendor/NONEXISTENT/products", headers=auth_headers
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
data = response.json()
|
|
assert data["error_code"] == "VENDOR_NOT_FOUND"
|
|
assert data["status_code"] == 404
|
|
assert "NONEXISTENT" in data["message"]
|
|
|
|
def test_vendor_not_active_business_logic_error(
|
|
self, client, auth_headers, test_vendor, db
|
|
):
|
|
"""Test accessing inactive vendor returns VendorNotActiveException (if enforced)"""
|
|
# Set vendor to inactive
|
|
test_vendor.is_active = False
|
|
db.commit()
|
|
|
|
# Depending on your business logic, this might return an error
|
|
response = client.get(
|
|
f"/api/v1/vendor/{test_vendor.vendor_code}", headers=auth_headers
|
|
)
|
|
|
|
# If your service enforces active vendor requirement
|
|
if response.status_code == 400:
|
|
data = response.json()
|
|
assert data["error_code"] == "VENDOR_NOT_ACTIVE"
|
|
assert data["status_code"] == 400
|
|
assert test_vendor.vendor_code in data["message"]
|
|
|
|
def test_vendor_not_verified_business_logic_error(
|
|
self, client, auth_headers, test_vendor, db
|
|
):
|
|
"""Test operations requiring verification returns VendorNotVerifiedException (if enforced)"""
|
|
# Set vendor to unverified
|
|
test_vendor.is_verified = False
|
|
db.commit()
|
|
|
|
# Test adding products (might require verification)
|
|
product_data = {
|
|
"marketplace_product_id": 1,
|
|
"price": 29.99,
|
|
}
|
|
|
|
response = client.post(
|
|
f"/api/v1/vendor/{test_vendor.vendor_code}/products",
|
|
headers=auth_headers,
|
|
json=product_data,
|
|
)
|
|
|
|
# If your service requires verification for adding products
|
|
if response.status_code == 400:
|
|
data = response.json()
|
|
assert data["error_code"] == "VENDOR_NOT_VERIFIED"
|
|
assert data["status_code"] == 400
|
|
assert test_vendor.vendor_code in data["message"]
|
|
|
|
def test_get_vendor_without_auth_returns_invalid_token(self, client):
|
|
"""Test that vendor endpoints require authentication returns InvalidTokenException"""
|
|
response = client.get("/api/v1/vendor")
|
|
|
|
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/vendor?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/vendor?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/vendor?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 vendor exceptions follow the consistent WizamartException structure"""
|
|
# Test with a known error case
|
|
response = client.get("/api/v1/vendor/NONEXISTENT", headers=auth_headers)
|
|
|
|
assert response.status_code == 404
|
|
data = response.json()
|
|
|
|
# Verify exception structure matches WizamartException.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)
|