# tests/unit/services/test_marketplace_service.py """Unit tests for MarketplaceImportJobService.""" import uuid import pytest from sqlalchemy.exc import SQLAlchemyError from app.modules.marketplace.exceptions import ( ImportJobNotFoundException, ImportJobNotOwnedException, ImportValidationError, ) from app.modules.marketplace.models import MarketplaceImportJob from app.modules.marketplace.schemas import MarketplaceImportJobRequest from app.modules.marketplace.services.marketplace_import_job_service import ( MarketplaceImportJobService, ) from app.modules.tenancy.exceptions import UnauthorizedStoreAccessException @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_store, 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_store, test_user) assert result.marketplace == "Amazon" assert result.store_id == test_store.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_store, 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_store, test_user) assert result.marketplace == "Letzshop" # Default def test_create_import_job_database_error( self, db, test_store, 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 SQLAlchemyError("Database flush failed") monkeypatch.setattr(db, "flush", mock_flush) with pytest.raises(ImportValidationError) as exc_info: self.service.create_import_job(db, request, test_store, test_user) exception = exc_info.value assert exception.error_code == "IMPORT_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 SQLAlchemyError("Database query failed") monkeypatch.setattr(db, "query", mock_query) with pytest.raises(ImportValidationError) as exc_info: self.service.get_import_job_by_id(db, 1, test_user) exception = exc_info.value assert exception.error_code == "IMPORT_VALIDATION_ERROR" # ==================== get_import_job_for_store Tests ==================== def test_get_import_job_for_store_success( self, db, test_marketplace_import_job, test_store ): """Test getting import job for store.""" result = self.service.get_import_job_for_store( db, test_marketplace_import_job.id, test_store.id ) assert result.id == test_marketplace_import_job.id assert result.store_id == test_store.id def test_get_import_job_for_store_not_found(self, db, test_store): """Test getting non-existent import job for store.""" with pytest.raises(ImportJobNotFoundException) as exc_info: self.service.get_import_job_for_store(db, 99999, test_store.id) exception = exc_info.value assert exception.error_code == "IMPORT_JOB_NOT_FOUND" def test_get_import_job_for_store_wrong_store( self, db, test_marketplace_import_job, other_user, other_merchant ): """Test getting import job for wrong store.""" from app.modules.tenancy.models import Store # Create another store unique_id = str(uuid.uuid4())[:8] other_store = Store( merchant_id=other_merchant.id, store_code=f"OTHER_{unique_id.upper()}", subdomain=f"other{unique_id.lower()}", name=f"Other Store {unique_id}", is_active=True, ) db.add(other_store) db.commit() db.refresh(other_store) with pytest.raises(UnauthorizedStoreAccessException): self.service.get_import_job_for_store( db, test_marketplace_import_job.id, other_store.id ) # ==================== get_import_jobs Tests ==================== def test_get_import_jobs_success( self, db, test_marketplace_import_job, test_store, test_user ): """Test getting import jobs for store.""" jobs = self.service.get_import_jobs(db, test_store, 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_store_jobs( self, db, test_marketplace_import_job, test_store, test_admin ): """Test that admin sees all store jobs.""" jobs = self.service.get_import_jobs(db, test_store, 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_store, test_user ): """Test getting import jobs with marketplace filter.""" jobs = self.service.get_import_jobs( db, test_store, 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_store, 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}", store_id=test_store.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) # noqa: PERF006 db.commit() jobs = self.service.get_import_jobs(db, test_store, test_user, skip=2, limit=2) assert len(jobs) <= 2 def test_get_import_jobs_empty(self, db, test_user, other_user, other_merchant): """Test getting import jobs when none exist.""" from app.modules.tenancy.models import Store # Create a store with no jobs unique_id = str(uuid.uuid4())[:8] empty_store = Store( merchant_id=other_merchant.id, store_code=f"EMPTY_{unique_id.upper()}", subdomain=f"empty{unique_id.lower()}", name=f"Empty Store {unique_id}", is_active=True, ) db.add(empty_store) db.commit() db.refresh(empty_store) jobs = self.service.get_import_jobs(db, empty_store, other_user) assert len(jobs) == 0 def test_get_import_jobs_database_error( self, db, test_store, test_user, monkeypatch ): """Test get import jobs handles database errors.""" def mock_query(*args): raise SQLAlchemyError("Database query failed") monkeypatch.setattr(db, "query", mock_query) with pytest.raises(ImportValidationError) as exc_info: self.service.get_import_jobs(db, test_store, test_user) exception = exc_info.value assert exception.error_code == "IMPORT_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_store ): """Test converting database model to response model.""" from app.modules.marketplace.models 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.store_id == job.store_id assert response.imported == (job.imported_count or 0) def test_convert_to_response_model_with_all_fields( self, db, test_store, test_user ): """Test converting model with all fields populated.""" str(uuid.uuid4())[:8] from datetime import datetime job = MarketplaceImportJob( status="completed", marketplace="TestMarket", store_id=test_store.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) # noqa: SEC034 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"