Files
orion/tests/unit/services/test_marketplace_service.py
Samir Boulahtit 9920430b9e 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>
2025-12-13 22:59:51 +01:00

410 lines
15 KiB
Python

# tests/unit/services/test_marketplace_service.py
"""Unit tests for MarketplaceImportJobService."""
import uuid
import pytest
from app.exceptions.base import ValidationException
from app.exceptions.marketplace_import_job import (
ImportJobNotFoundException,
ImportJobNotOwnedException,
)
from app.exceptions.vendor import UnauthorizedVendorAccessException
from app.services.marketplace_import_job_service import MarketplaceImportJobService
from models.database.marketplace_import_job import MarketplaceImportJob
from models.schema.marketplace_import_job import MarketplaceImportJobRequest
@pytest.mark.unit
@pytest.mark.marketplace
class TestMarketplaceImportJobService:
"""Test suite for MarketplaceImportJobService."""
def setup_method(self):
self.service = MarketplaceImportJobService()
# ==================== create_import_job Tests ====================
def test_create_import_job_success(self, db, test_vendor, test_user):
"""Test successful creation of import job."""
request = MarketplaceImportJobRequest(
source_url="https://example.com/products.csv",
marketplace="Amazon",
batch_size=1000,
)
result = self.service.create_import_job(db, request, test_vendor, test_user)
assert result.marketplace == "Amazon"
assert result.vendor_id == test_vendor.id
assert result.user_id == test_user.id
assert result.status == "pending"
assert result.source_url == "https://example.com/products.csv"
def test_create_import_job_default_marketplace(self, db, test_vendor, test_user):
"""Test import job creation with default marketplace."""
request = MarketplaceImportJobRequest(
source_url="https://example.com/products.csv",
)
result = self.service.create_import_job(db, request, test_vendor, test_user)
assert result.marketplace == "Letzshop" # Default
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",
marketplace="Amazon",
)
def mock_flush():
raise Exception("Database flush failed")
monkeypatch.setattr(db, "flush", mock_flush)
with pytest.raises(ValidationException) as exc_info:
self.service.create_import_job(db, request, test_vendor, test_user)
exception = exc_info.value
assert exception.error_code == "VALIDATION_ERROR"
assert "Failed to create import job" in exception.message
# ==================== get_import_job_by_id Tests ====================
def test_get_import_job_by_id_success(
self, db, test_marketplace_import_job, test_user
):
"""Test getting import job by ID for job owner."""
result = self.service.get_import_job_by_id(
db, test_marketplace_import_job.id, test_user
)
assert result.id == test_marketplace_import_job.id
assert result.user_id == test_user.id
def test_get_import_job_by_id_admin_access(
self, db, test_marketplace_import_job, test_admin
):
"""Test that admin can access any import job."""
result = self.service.get_import_job_by_id(
db, test_marketplace_import_job.id, test_admin
)
assert result.id == test_marketplace_import_job.id
def test_get_import_job_by_id_not_found(self, db, test_user):
"""Test getting non-existent import job."""
with pytest.raises(ImportJobNotFoundException) as exc_info:
self.service.get_import_job_by_id(db, 99999, test_user)
exception = exc_info.value
assert exception.error_code == "IMPORT_JOB_NOT_FOUND"
assert exception.status_code == 404
assert "99999" in exception.message
def test_get_import_job_by_id_access_denied(
self, db, test_marketplace_import_job, other_user
):
"""Test access denied when user doesn't own the job."""
with pytest.raises(ImportJobNotOwnedException) as exc_info:
self.service.get_import_job_by_id(
db, test_marketplace_import_job.id, other_user
)
exception = exc_info.value
assert exception.error_code == "IMPORT_JOB_NOT_OWNED"
assert exception.status_code == 403
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")
monkeypatch.setattr(db, "query", mock_query)
with pytest.raises(ValidationException) as exc_info:
self.service.get_import_job_by_id(db, 1, test_user)
exception = exc_info.value
assert exception.error_code == "VALIDATION_ERROR"
# ==================== get_import_job_for_vendor Tests ====================
def test_get_import_job_for_vendor_success(
self, db, test_marketplace_import_job, test_vendor
):
"""Test getting import job for vendor."""
result = self.service.get_import_job_for_vendor(
db, test_marketplace_import_job.id, test_vendor.id
)
assert result.id == test_marketplace_import_job.id
assert result.vendor_id == test_vendor.id
def test_get_import_job_for_vendor_not_found(self, db, test_vendor):
"""Test getting non-existent import job for vendor."""
with pytest.raises(ImportJobNotFoundException) as exc_info:
self.service.get_import_job_for_vendor(db, 99999, test_vendor.id)
exception = exc_info.value
assert exception.error_code == "IMPORT_JOB_NOT_FOUND"
def test_get_import_job_for_vendor_wrong_vendor(
self, db, test_marketplace_import_job, other_user, other_company
):
"""Test getting import job for wrong vendor."""
from models.database.vendor import Vendor
# Create another vendor
unique_id = str(uuid.uuid4())[:8]
other_vendor = Vendor(
company_id=other_company.id,
vendor_code=f"OTHER_{unique_id.upper()}",
subdomain=f"other{unique_id.lower()}",
name=f"Other Vendor {unique_id}",
is_active=True,
)
db.add(other_vendor)
db.commit()
db.refresh(other_vendor)
with pytest.raises(UnauthorizedVendorAccessException):
self.service.get_import_job_for_vendor(
db, test_marketplace_import_job.id, other_vendor.id
)
# ==================== get_import_jobs Tests ====================
def test_get_import_jobs_success(
self, db, test_marketplace_import_job, test_vendor, test_user
):
"""Test getting import jobs for vendor."""
jobs = self.service.get_import_jobs(db, test_vendor, test_user)
assert len(jobs) >= 1
assert any(job.id == test_marketplace_import_job.id for job in jobs)
def test_get_import_jobs_admin_sees_all_vendor_jobs(
self, db, test_marketplace_import_job, test_vendor, test_admin
):
"""Test that admin sees all vendor jobs."""
jobs = self.service.get_import_jobs(db, test_vendor, test_admin)
assert len(jobs) >= 1
assert any(job.id == test_marketplace_import_job.id for job in jobs)
def test_get_import_jobs_with_marketplace_filter(
self, db, test_marketplace_import_job, test_vendor, test_user
):
"""Test getting import jobs with marketplace filter."""
jobs = self.service.get_import_jobs(
db,
test_vendor,
test_user,
marketplace=test_marketplace_import_job.marketplace,
)
assert len(jobs) >= 1
assert all(
test_marketplace_import_job.marketplace.lower() in job.marketplace.lower()
for job in jobs
)
def test_get_import_jobs_with_pagination(self, db, test_vendor, test_user):
"""Test getting import jobs with pagination."""
unique_id = str(uuid.uuid4())[:8]
# Create multiple import jobs
for i in range(5):
job = MarketplaceImportJob(
status="completed",
marketplace=f"Marketplace_{unique_id}_{i}",
vendor_id=test_vendor.id,
user_id=test_user.id,
source_url=f"https://test-{i}.example.com/import",
imported_count=0,
updated_count=0,
total_processed=0,
error_count=0,
)
db.add(job)
db.commit()
jobs = self.service.get_import_jobs(db, test_vendor, test_user, skip=2, limit=2)
assert len(jobs) <= 2
def test_get_import_jobs_empty(self, db, test_user, other_user, other_company):
"""Test getting import jobs when none exist."""
from models.database.vendor import Vendor
# Create a vendor with no jobs
unique_id = str(uuid.uuid4())[:8]
empty_vendor = Vendor(
company_id=other_company.id,
vendor_code=f"EMPTY_{unique_id.upper()}",
subdomain=f"empty{unique_id.lower()}",
name=f"Empty Vendor {unique_id}",
is_active=True,
)
db.add(empty_vendor)
db.commit()
db.refresh(empty_vendor)
jobs = self.service.get_import_jobs(db, empty_vendor, other_user)
assert len(jobs) == 0
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")
monkeypatch.setattr(db, "query", mock_query)
with pytest.raises(ValidationException) as exc_info:
self.service.get_import_jobs(db, test_vendor, test_user)
exception = exc_info.value
assert exception.error_code == "VALIDATION_ERROR"
assert "Failed to retrieve import jobs" in exception.message
# ==================== convert_to_response_model Tests ====================
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
# Re-query to get fresh instance with relationships
job = db.query(MIJ).filter(MIJ.id == test_marketplace_import_job.id).first()
response = self.service.convert_to_response_model(job)
assert response.job_id == job.id
assert response.status == job.status
assert response.marketplace == job.marketplace
assert response.vendor_id == job.vendor_id
assert response.imported == (job.imported_count or 0)
def test_convert_to_response_model_with_all_fields(
self, db, test_vendor, test_user
):
"""Test converting model with all fields populated."""
unique_id = str(uuid.uuid4())[:8]
from datetime import datetime
job = MarketplaceImportJob(
status="completed",
marketplace="TestMarket",
vendor_id=test_vendor.id,
user_id=test_user.id,
source_url="https://test.example.com/import",
imported_count=100,
updated_count=50,
total_processed=150,
error_count=5,
error_message="Some errors occurred",
started_at=datetime.utcnow(),
completed_at=datetime.utcnow(),
)
db.add(job)
db.commit()
db.refresh(job)
response = self.service.convert_to_response_model(job)
assert response.imported == 100
assert response.updated == 50
assert response.total_processed == 150
assert response.error_count == 5
assert response.error_message == "Some errors occurred"
assert response.started_at is not None
assert response.completed_at is not None
@pytest.mark.unit
@pytest.mark.marketplace
class TestMarketplaceImportJobSchema:
"""Test suite for MarketplaceImportJobRequest schema validation."""
def test_valid_request(self):
"""Test valid request schema."""
request = MarketplaceImportJobRequest(
source_url="https://example.com/products.csv",
marketplace="Amazon",
batch_size=1000,
)
assert request.source_url == "https://example.com/products.csv"
assert request.marketplace == "Amazon"
assert request.batch_size == 1000
def test_default_values(self):
"""Test default values for optional fields."""
request = MarketplaceImportJobRequest(
source_url="https://example.com/products.csv",
)
assert request.marketplace == "Letzshop"
assert request.batch_size == 1000
def test_url_validation_http(self):
"""Test URL validation accepts http."""
request = MarketplaceImportJobRequest(
source_url="http://example.com/products.csv",
)
assert request.source_url == "http://example.com/products.csv"
def test_url_validation_invalid(self):
"""Test URL validation rejects invalid URLs."""
with pytest.raises(ValueError) as exc_info:
MarketplaceImportJobRequest(
source_url="ftp://example.com/products.csv",
)
assert "URL must start with http://" in str(exc_info.value)
def test_url_with_trailing_slash(self):
"""Test URL with trailing slash is accepted."""
request = MarketplaceImportJobRequest(
source_url="https://example.com/products.csv/",
)
assert request.source_url == "https://example.com/products.csv/"
def test_batch_size_validation_min(self):
"""Test batch_size validation minimum."""
with pytest.raises(ValueError):
MarketplaceImportJobRequest(
source_url="https://example.com/products.csv",
batch_size=50, # Below minimum of 100
)
def test_batch_size_validation_max(self):
"""Test batch_size validation maximum."""
with pytest.raises(ValueError):
MarketplaceImportJobRequest(
source_url="https://example.com/products.csv",
batch_size=20000, # Above maximum of 10000
)
def test_marketplace_strips_whitespace(self):
"""Test marketplace strips whitespace."""
request = MarketplaceImportJobRequest(
source_url="https://example.com/products.csv",
marketplace=" Amazon ",
)
assert request.marketplace == "Amazon"