fix: correct tojson|safe usage in templates and update validator

- Remove |safe from |tojson in HTML attributes (x-data) - quotes must
  become " for browsers to parse correctly
- Update LANG-002 and LANG-003 architecture rules to document correct
  |tojson usage patterns:
  - HTML attributes: |tojson (no |safe)
  - Script blocks: |tojson|safe
- Fix validator to warn when |tojson|safe is used in x-data (breaks
  HTML attribute parsing)
- Improve code quality across services, APIs, and tests

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-13 22:59:51 +01:00
parent 94d268f330
commit 9920430b9e
123 changed files with 1408 additions and 840 deletions

View File

@@ -122,7 +122,9 @@ class TestAdminService:
from models.database.vendor import Vendor
# Re-query vendor to get fresh instance
vendor_to_unverify = db.query(Vendor).filter(Vendor.id == test_vendor.id).first()
vendor_to_unverify = (
db.query(Vendor).filter(Vendor.id == test_vendor.id).first()
)
vendor_to_unverify.is_verified = False
db.commit()
@@ -186,7 +188,10 @@ class TestAdminService:
None,
)
assert test_job is not None
assert test_job.marketplace.lower() == test_marketplace_import_job.marketplace.lower()
assert (
test_job.marketplace.lower()
== test_marketplace_import_job.marketplace.lower()
)
assert test_job.status == test_marketplace_import_job.status
def test_get_marketplace_import_jobs_with_marketplace_filter(

View File

@@ -1,5 +1,6 @@
# tests/unit/services/test_auth_service.py
"""Unit tests for AuthService - login and password hashing."""
import pytest
from app.exceptions.auth import (

View File

@@ -1,5 +1,6 @@
# tests/unit/services/test_inventory_service.py
"""Unit tests for InventoryService."""
import uuid
import pytest
@@ -352,23 +353,21 @@ class TestInventoryService:
self, db, test_inventory, test_product, test_vendor
):
"""Test getting product inventory summary."""
result = self.service.get_product_inventory(
db, test_vendor.id, test_product.id
)
result = self.service.get_product_inventory(db, test_vendor.id, test_product.id)
assert result.product_id == test_product.id
assert result.vendor_id == test_vendor.id
assert result.total_quantity >= test_inventory.quantity
assert len(result.locations) >= 1
def test_get_product_inventory_no_inventory(
self, db, test_product, test_vendor
):
def test_get_product_inventory_no_inventory(self, db, test_product, test_vendor):
"""Test getting inventory for product with no inventory entries."""
# Create a new product without inventory
from models.database.product import Product
from models.database.marketplace_product import MarketplaceProduct
from models.database.marketplace_product_translation import MarketplaceProductTranslation
from models.database.marketplace_product_translation import (
MarketplaceProductTranslation,
)
from models.database.product import Product
unique_id = str(uuid.uuid4())[:8]
mp = MarketplaceProduct(
@@ -412,9 +411,7 @@ class TestInventoryService:
# ==================== Get Vendor Inventory Tests ====================
def test_get_vendor_inventory_success(
self, db, test_inventory, test_vendor
):
def test_get_vendor_inventory_success(self, db, test_inventory, test_vendor):
"""Test getting all vendor inventory."""
result = self.service.get_vendor_inventory(db, test_vendor.id)
@@ -433,9 +430,7 @@ class TestInventoryService:
for inv in result:
assert test_inventory.location[:10].upper() in inv.location.upper()
def test_get_vendor_inventory_with_low_stock_filter(
self, db, test_vendor
):
def test_get_vendor_inventory_with_low_stock_filter(self, db, test_vendor):
"""Test getting vendor inventory filtered by low stock threshold."""
result = self.service.get_vendor_inventory(
db, test_vendor.id, low_stock_threshold=5
@@ -446,17 +441,13 @@ class TestInventoryService:
def test_get_vendor_inventory_pagination(self, db, test_vendor):
"""Test vendor inventory pagination."""
result = self.service.get_vendor_inventory(
db, test_vendor.id, skip=0, limit=10
)
result = self.service.get_vendor_inventory(db, test_vendor.id, skip=0, limit=10)
assert len(result) <= 10
# ==================== Update Inventory Tests ====================
def test_update_inventory_quantity(
self, db, test_inventory, test_vendor
):
def test_update_inventory_quantity(self, db, test_inventory, test_vendor):
"""Test updating inventory quantity."""
inventory_update = InventoryUpdate(quantity=500)
@@ -466,9 +457,7 @@ class TestInventoryService:
assert result.quantity == 500
def test_update_inventory_reserved_quantity(
self, db, test_inventory, test_vendor
):
def test_update_inventory_reserved_quantity(self, db, test_inventory, test_vendor):
"""Test updating inventory reserved quantity."""
inventory_update = InventoryUpdate(reserved_quantity=20)
@@ -478,9 +467,7 @@ class TestInventoryService:
assert result.reserved_quantity == 20
def test_update_inventory_location(
self, db, test_inventory, test_vendor
):
def test_update_inventory_location(self, db, test_inventory, test_vendor):
"""Test updating inventory location."""
unique_id = str(uuid.uuid4())[:8].upper()
new_location = f"NEW_LOCATION_{unique_id}"
@@ -499,9 +486,7 @@ class TestInventoryService:
with pytest.raises(InventoryNotFoundException):
self.service.update_inventory(db, test_vendor.id, 99999, inventory_update)
def test_update_inventory_wrong_vendor(
self, db, test_inventory, other_company
):
def test_update_inventory_wrong_vendor(self, db, test_inventory, other_company):
"""Test updating inventory from wrong vendor raises InventoryNotFoundException."""
from models.database.vendor import Vendor
@@ -525,9 +510,7 @@ class TestInventoryService:
# ==================== Delete Inventory Tests ====================
def test_delete_inventory_success(
self, db, test_inventory, test_vendor
):
def test_delete_inventory_success(self, db, test_inventory, test_vendor):
"""Test deleting inventory entry."""
inventory_id = test_inventory.id
@@ -544,9 +527,7 @@ class TestInventoryService:
with pytest.raises(InventoryNotFoundException):
self.service.delete_inventory(db, test_vendor.id, 99999)
def test_delete_inventory_wrong_vendor(
self, db, test_inventory, other_company
):
def test_delete_inventory_wrong_vendor(self, db, test_inventory, other_company):
"""Test deleting inventory from wrong vendor raises InventoryNotFoundException."""
from models.database.vendor import Vendor
@@ -563,4 +544,3 @@ class TestInventoryService:
with pytest.raises(InventoryNotFoundException):
self.service.delete_inventory(db, other_vendor.id, test_inventory.id)

View File

@@ -8,26 +8,22 @@ Tests cover:
- GraphQL client (mocked)
"""
import pytest
from unittest.mock import MagicMock, patch
import pytest
from app.services.letzshop import (
LetzshopClient,
LetzshopClientError,
LetzshopAuthError,
LetzshopAPIError,
LetzshopCredentialsService,
CredentialsNotFoundError,
LetzshopAPIError,
LetzshopClient,
LetzshopCredentialsService,
)
from app.utils.encryption import (
EncryptionService,
EncryptionError,
encrypt_value,
decrypt_value,
EncryptionService,
mask_api_key,
)
# ============================================================================
# Encryption Tests
# ============================================================================
@@ -339,9 +335,7 @@ class TestLetzshopClient:
"""Test successful connection test."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"data": {"__typename": "Query"}
}
mock_response.json.return_value = {"data": {"__typename": "Query"}}
mock_post.return_value = mock_response
client = LetzshopClient(api_key="test-key")
@@ -398,7 +392,7 @@ class TestLetzshopClient:
"inventoryUnits": [
{"id": "unit_1", "state": "confirmed"},
],
"errors": []
"errors": [],
}
}
}
@@ -420,12 +414,9 @@ class TestLetzshopClient:
"setShipmentTracking": {
"shipment": {
"id": "ship_1",
"tracking": {
"code": "1Z999AA1",
"provider": "ups"
}
"tracking": {"code": "1Z999AA1", "provider": "ups"},
},
"errors": []
"errors": [],
}
}
}
@@ -446,9 +437,7 @@ class TestLetzshopClient:
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"errors": [
{"message": "Invalid shipment ID"}
]
"errors": [{"message": "Invalid shipment ID"}]
}
mock_post.return_value = mock_response

View File

@@ -1,9 +1,9 @@
# tests/unit/services/test_marketplace_service.py
"""Unit tests for MarketplaceImportJobService."""
import uuid
import pytest
from unittest.mock import patch
from app.exceptions.base import ValidationException
from app.exceptions.marketplace_import_job import (
@@ -52,7 +52,9 @@ class TestMarketplaceImportJobService:
assert result.marketplace == "Letzshop" # Default
def test_create_import_job_database_error(self, db, test_vendor, test_user, monkeypatch):
def test_create_import_job_database_error(
self, db, test_vendor, test_user, monkeypatch
):
"""Test import job creation handles database errors."""
request = MarketplaceImportJobRequest(
source_url="https://example.com/products.csv",
@@ -119,6 +121,7 @@ class TestMarketplaceImportJobService:
def test_get_import_job_by_id_database_error(self, db, test_user, monkeypatch):
"""Test get import job handles database errors."""
def mock_query(*args):
raise Exception("Database query failed")
@@ -200,7 +203,10 @@ class TestMarketplaceImportJobService:
):
"""Test getting import jobs with marketplace filter."""
jobs = self.service.get_import_jobs(
db, test_vendor, test_user, marketplace=test_marketplace_import_job.marketplace
db,
test_vendor,
test_user,
marketplace=test_marketplace_import_job.marketplace,
)
assert len(jobs) >= 1
@@ -254,8 +260,11 @@ class TestMarketplaceImportJobService:
assert len(jobs) == 0
def test_get_import_jobs_database_error(self, db, test_vendor, test_user, monkeypatch):
def test_get_import_jobs_database_error(
self, db, test_vendor, test_user, monkeypatch
):
"""Test get import jobs handles database errors."""
def mock_query(*args):
raise Exception("Database query failed")
@@ -270,7 +279,9 @@ class TestMarketplaceImportJobService:
# ==================== convert_to_response_model Tests ====================
def test_convert_to_response_model(self, db, test_marketplace_import_job, test_vendor):
def test_convert_to_response_model(
self, db, test_marketplace_import_job, test_vendor
):
"""Test converting database model to response model."""
from models.database.marketplace_import_job import MarketplaceImportJob as MIJ

View File

@@ -108,8 +108,7 @@ class TestProductService:
assert existing_product_id in str(exc_info.value)
assert exc_info.value.status_code == 409
assert (
exc_info.value.details.get("marketplace_product_id")
== existing_product_id
exc_info.value.details.get("marketplace_product_id") == existing_product_id
)
def test_create_product_invalid_price(self, db):
@@ -224,7 +223,9 @@ class TestProductService:
assert "Invalid GTIN format" in str(exc_info.value)
assert exc_info.value.details.get("field") == "gtin"
def test_update_product_empty_title_preserves_existing(self, db, test_marketplace_product):
def test_update_product_empty_title_preserves_existing(
self, db, test_marketplace_product
):
"""Test updating product with empty title preserves existing title in translation"""
original_title = test_marketplace_product.get_title()
update_data = MarketplaceProductUpdate(title="")
@@ -275,7 +276,9 @@ class TestProductService:
self, db, test_marketplace_product_with_inventory
):
"""Test getting inventory info for product with inventory."""
marketplace_product = test_marketplace_product_with_inventory["marketplace_product"]
marketplace_product = test_marketplace_product_with_inventory[
"marketplace_product"
]
inventory = test_marketplace_product_with_inventory["inventory"]
inventory_info = self.service.get_inventory_info(db, marketplace_product.gtin)
@@ -352,7 +355,10 @@ class TestMarketplaceProductServiceAdmin:
# Find our test product in results
found = False
for p in products:
if p["marketplace_product_id"] == test_marketplace_product.marketplace_product_id:
if (
p["marketplace_product_id"]
== test_marketplace_product.marketplace_product_id
):
found = True
assert p["id"] == test_marketplace_product.id
assert p["marketplace"] == test_marketplace_product.marketplace
@@ -429,9 +435,7 @@ class TestMarketplaceProductServiceAdmin:
def test_get_admin_product_detail(self, db, test_marketplace_product):
"""Test getting admin product detail by ID."""
product = self.service.get_admin_product_detail(
db, test_marketplace_product.id
)
product = self.service.get_admin_product_detail(db, test_marketplace_product.id)
assert product["id"] == test_marketplace_product.id
assert (

View File

@@ -1,19 +1,19 @@
# tests/unit/services/test_stats_service.py
"""Unit tests for StatsService following the application's testing patterns."""
import uuid
from unittest.mock import patch
import pytest
from sqlalchemy.exc import SQLAlchemyError
from unittest.mock import patch, MagicMock
from app.exceptions import AdminOperationException, VendorNotFoundException
from app.services.stats_service import StatsService
from models.database.inventory import Inventory
from models.database.marketplace_import_job import MarketplaceImportJob
from models.database.marketplace_product import MarketplaceProduct
from models.database.marketplace_product_translation import MarketplaceProductTranslation
from models.database.product import Product
from models.database.vendor import Vendor
from models.database.marketplace_product_translation import (
MarketplaceProductTranslation,
)
def create_marketplace_product_with_translation(
@@ -21,8 +21,7 @@ def create_marketplace_product_with_translation(
):
"""Helper to create a MarketplaceProduct with its translation."""
product = MarketplaceProduct(
marketplace_product_id=marketplace_product_id,
**kwargs
marketplace_product_id=marketplace_product_id, **kwargs
)
db.add(product)
db.flush() # Get the product ID
@@ -275,7 +274,10 @@ class TestStatsService:
with pytest.raises(AdminOperationException) as exc_info:
self.service.get_marketplace_breakdown_stats(db)
assert exc_info.value.details.get("operation") == "get_marketplace_breakdown_stats"
assert (
exc_info.value.details.get("operation")
== "get_marketplace_breakdown_stats"
)
# ==================== get_vendor_stats Tests ====================
@@ -371,9 +373,7 @@ class TestStatsService:
stats = self.service.get_vendor_statistics(db)
if stats["total_vendors"] > 0:
expected_rate = (
stats["verified_vendors"] / stats["total_vendors"] * 100
)
expected_rate = stats["verified_vendors"] / stats["total_vendors"] * 100
assert abs(stats["verification_rate"] - expected_rate) < 0.01
def test_get_vendor_statistics_database_error(self, db):
@@ -438,9 +438,7 @@ class TestStatsService:
stats = self.service.get_import_statistics(db)
if stats["total_imports"] > 0:
expected_rate = (
stats["completed_imports"] / stats["total_imports"] * 100
)
expected_rate = stats["completed_imports"] / stats["total_imports"] * 100
assert abs(stats["success_rate"] - expected_rate) < 0.01
else:
assert stats["success_rate"] == 0

View File

@@ -4,6 +4,7 @@ Unit tests for VendorProductService.
Tests the vendor product catalog service operations.
"""
import pytest
from app.exceptions import ProductNotFoundException
@@ -31,7 +32,9 @@ class TestVendorProductService:
if p["id"] == test_product.id:
found = True
assert p["vendor_id"] == test_product.vendor_id
assert p["marketplace_product_id"] == test_product.marketplace_product_id
assert (
p["marketplace_product_id"] == test_product.marketplace_product_id
)
break
assert found, "Test product not found in results"

View File

@@ -1,5 +1,6 @@
# tests/unit/services/test_vendor_service.py
"""Unit tests for VendorService following the application's exception patterns."""
import uuid
import pytest
@@ -85,7 +86,9 @@ class TestVendorService:
assert vendor.is_verified is True # Admin creates verified vendor
def test_create_vendor_duplicate_code(self, db, test_user, test_company, test_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,
@@ -359,9 +362,11 @@ class TestVendorService:
# 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()
mp = (
db.query(MarketplaceProduct)
.filter(MarketplaceProduct.id == unique_product.id)
.first()
)
product_data = ProductCreate(
marketplace_product_id=mp.id,
@@ -394,6 +399,7 @@ class TestVendorService:
"""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()
@@ -469,7 +475,9 @@ class TestVendorServiceExceptionDetails:
def setup_method(self):
self.service = VendorService()
def test_exception_to_dict_structure(self, db, test_user, test_vendor, test_company):
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,