- Move Product/ProductTranslation to app/modules/catalog/models/ - Move VendorOnboarding to app/modules/marketplace/models/ - Delete legacy re-export files for marketplace models: - letzshop.py, marketplace.py, marketplace_product.py - marketplace_product_translation.py, marketplace_import_job.py - Delete legacy product.py, product_translation.py, onboarding.py - Update all imports across services, tasks, tests to use module locations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
380 lines
13 KiB
Python
380 lines
13 KiB
Python
# tests/integration/tasks/test_background_tasks.py
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from app.tasks.background_tasks import process_marketplace_import
|
|
from app.modules.marketplace.models import MarketplaceImportJob
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.database
|
|
@pytest.mark.marketplace
|
|
class TestBackgroundTasks:
|
|
@pytest.mark.asyncio
|
|
async def test_marketplace_import_success(self, db, test_user, test_vendor):
|
|
"""Test successful marketplace import background task"""
|
|
# Create import job
|
|
job = MarketplaceImportJob(
|
|
status="pending",
|
|
source_url="http://example.com/test.csv",
|
|
marketplace="TestMarket",
|
|
vendor_id=test_vendor.id,
|
|
user_id=test_user.id,
|
|
language="en",
|
|
)
|
|
db.add(job)
|
|
db.commit()
|
|
db.refresh(job)
|
|
|
|
# Store the job ID before it becomes detached
|
|
job_id = job.id
|
|
|
|
# Mock CSV processor and prevent session from closing
|
|
with (
|
|
patch("app.tasks.background_tasks.CSVProcessor") as mock_processor,
|
|
patch("app.tasks.background_tasks.SessionLocal", return_value=db),
|
|
):
|
|
mock_instance = mock_processor.return_value
|
|
mock_instance.process_marketplace_csv_from_url = AsyncMock(
|
|
return_value={
|
|
"imported": 10,
|
|
"updated": 5,
|
|
"total_processed": 15,
|
|
"errors": 0,
|
|
}
|
|
)
|
|
|
|
# Run background task
|
|
await process_marketplace_import(
|
|
job_id, "http://example.com/test.csv", "TestMarket", test_vendor.id, 1000
|
|
)
|
|
|
|
# Re-query the job using the stored ID
|
|
updated_job = (
|
|
db.query(MarketplaceImportJob)
|
|
.filter(MarketplaceImportJob.id == job_id)
|
|
.first()
|
|
)
|
|
|
|
assert updated_job is not None
|
|
assert updated_job.status == "completed"
|
|
assert updated_job.imported_count == 10
|
|
assert updated_job.updated_count == 5
|
|
assert updated_job.total_processed == 15
|
|
assert updated_job.error_count == 0
|
|
assert updated_job.started_at is not None
|
|
assert updated_job.completed_at is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_marketplace_import_failure(self, db, test_user, test_vendor):
|
|
"""Test marketplace import failure handling"""
|
|
# Create import job
|
|
job = MarketplaceImportJob(
|
|
status="pending",
|
|
source_url="http://example.com/test.csv",
|
|
marketplace="TestMarket",
|
|
vendor_id=test_vendor.id,
|
|
user_id=test_user.id,
|
|
language="en",
|
|
)
|
|
db.add(job)
|
|
db.commit()
|
|
db.refresh(job)
|
|
|
|
# Store the job ID before it becomes detached
|
|
job_id = job.id
|
|
|
|
# Mock CSV processor to raise exception
|
|
with (
|
|
patch("app.tasks.background_tasks.CSVProcessor") as mock_processor,
|
|
patch("app.tasks.background_tasks.SessionLocal", return_value=db),
|
|
):
|
|
mock_instance = mock_processor.return_value
|
|
mock_instance.process_marketplace_csv_from_url = AsyncMock(
|
|
side_effect=Exception("Import failed")
|
|
)
|
|
|
|
# Run background task - this should not raise the exception
|
|
# because it's handled in the background task
|
|
try:
|
|
await process_marketplace_import(
|
|
job_id,
|
|
"http://example.com/test.csv",
|
|
"TestMarket",
|
|
test_vendor.id,
|
|
1000,
|
|
)
|
|
except Exception:
|
|
# The background task should handle exceptions internally
|
|
# If an exception propagates here, that's a bug in the background task
|
|
pass
|
|
|
|
# Re-query the job using the stored ID
|
|
updated_job = (
|
|
db.query(MarketplaceImportJob)
|
|
.filter(MarketplaceImportJob.id == job_id)
|
|
.first()
|
|
)
|
|
|
|
assert updated_job is not None
|
|
assert updated_job.status == "failed"
|
|
assert "Import failed" in updated_job.error_message
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_marketplace_import_job_not_found(self, db):
|
|
"""Test handling when import job doesn't exist"""
|
|
with (
|
|
patch("app.tasks.background_tasks.CSVProcessor") as mock_processor,
|
|
patch("app.tasks.background_tasks.SessionLocal", return_value=db),
|
|
):
|
|
mock_instance = mock_processor.return_value
|
|
mock_instance.process_marketplace_csv_from_url = AsyncMock(
|
|
return_value={
|
|
"imported": 10,
|
|
"updated": 5,
|
|
"total_processed": 15,
|
|
"errors": 0,
|
|
}
|
|
)
|
|
|
|
# Run background task with non-existent job ID
|
|
await process_marketplace_import(
|
|
999, # Non-existent job ID
|
|
"http://example.com/test.csv",
|
|
"TestMarket",
|
|
"TESTVENDOR",
|
|
1000,
|
|
)
|
|
|
|
# Should not raise an exception, just log and return
|
|
# The CSV processor should not be called
|
|
mock_instance.process_marketplace_csv_from_url.assert_not_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_marketplace_import_with_errors(self, db, test_user, test_vendor):
|
|
"""Test marketplace import with some errors"""
|
|
# Create import job
|
|
job = MarketplaceImportJob(
|
|
status="pending",
|
|
source_url="http://example.com/test.csv",
|
|
marketplace="TestMarket",
|
|
vendor_id=test_vendor.id,
|
|
user_id=test_user.id,
|
|
language="en",
|
|
)
|
|
db.add(job)
|
|
db.commit()
|
|
db.refresh(job)
|
|
|
|
# Store the job ID before it becomes detached
|
|
job_id = job.id
|
|
|
|
# Mock CSV processor with some errors
|
|
with (
|
|
patch("app.tasks.background_tasks.CSVProcessor") as mock_processor,
|
|
patch("app.tasks.background_tasks.SessionLocal", return_value=db),
|
|
):
|
|
mock_instance = mock_processor.return_value
|
|
mock_instance.process_marketplace_csv_from_url = AsyncMock(
|
|
return_value={
|
|
"imported": 8,
|
|
"updated": 5,
|
|
"total_processed": 15,
|
|
"errors": 2,
|
|
}
|
|
)
|
|
|
|
# Run background task
|
|
await process_marketplace_import(
|
|
job_id, "http://example.com/test.csv", "TestMarket", test_vendor.id, 1000
|
|
)
|
|
|
|
# Re-query the job using the stored ID
|
|
updated_job = (
|
|
db.query(MarketplaceImportJob)
|
|
.filter(MarketplaceImportJob.id == job_id)
|
|
.first()
|
|
)
|
|
|
|
assert updated_job is not None
|
|
assert updated_job.status == "completed_with_errors"
|
|
assert updated_job.imported_count == 8
|
|
assert updated_job.updated_count == 5
|
|
assert updated_job.error_count == 2
|
|
assert updated_job.total_processed == 15
|
|
assert "2 rows had errors" in updated_job.error_message
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_marketplace_import_vendor_not_found(self, db, test_user, test_vendor):
|
|
"""Test handling when vendor doesn't exist"""
|
|
# Create import job
|
|
job = MarketplaceImportJob(
|
|
status="pending",
|
|
source_url="http://example.com/test.csv",
|
|
marketplace="TestMarket",
|
|
vendor_id=test_vendor.id,
|
|
user_id=test_user.id,
|
|
language="en",
|
|
)
|
|
db.add(job)
|
|
db.commit()
|
|
db.refresh(job)
|
|
|
|
# Store the job ID before it becomes detached
|
|
job_id = job.id
|
|
|
|
# Mock CSV processor
|
|
with (
|
|
patch("app.tasks.background_tasks.CSVProcessor") as mock_processor,
|
|
patch("app.tasks.background_tasks.SessionLocal", return_value=db),
|
|
):
|
|
mock_instance = mock_processor.return_value
|
|
mock_instance.process_marketplace_csv_from_url = AsyncMock(
|
|
return_value={
|
|
"imported": 10,
|
|
"updated": 5,
|
|
"total_processed": 15,
|
|
"errors": 0,
|
|
}
|
|
)
|
|
|
|
# Run background task with non-existent vendor ID
|
|
await process_marketplace_import(
|
|
job_id,
|
|
"http://example.com/test.csv",
|
|
"TestMarket",
|
|
99999, # Non-existent vendor ID
|
|
1000,
|
|
)
|
|
|
|
# The CSV processor should not be called
|
|
mock_instance.process_marketplace_csv_from_url.assert_not_called()
|
|
|
|
# Re-query the job using the stored ID
|
|
updated_job = (
|
|
db.query(MarketplaceImportJob)
|
|
.filter(MarketplaceImportJob.id == job_id)
|
|
.first()
|
|
)
|
|
|
|
assert updated_job is not None
|
|
assert updated_job.status == "failed"
|
|
assert "Vendor 99999 not found" in updated_job.error_message
|
|
assert updated_job.completed_at is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_marketplace_import_commit_error_handling(
|
|
self, db, test_user, test_vendor
|
|
):
|
|
"""Test handling when commit fails during exception handling"""
|
|
# Create import job
|
|
job = MarketplaceImportJob(
|
|
status="pending",
|
|
source_url="http://example.com/test.csv",
|
|
marketplace="TestMarket",
|
|
vendor_id=test_vendor.id,
|
|
user_id=test_user.id,
|
|
language="en",
|
|
)
|
|
db.add(job)
|
|
db.commit()
|
|
db.refresh(job)
|
|
|
|
# Store the job ID before it becomes detached
|
|
job_id = job.id
|
|
|
|
# Create a mock session that will fail on commit in the exception handler
|
|
mock_session = MagicMock()
|
|
mock_session.query.return_value.filter.return_value.first.side_effect = [
|
|
job, # First call returns the job
|
|
test_vendor, # Second call returns the vendor
|
|
]
|
|
mock_session.commit.side_effect = [
|
|
None, # First commit (status=processing) succeeds
|
|
Exception("Simulated commit failure"), # Second commit fails
|
|
]
|
|
mock_session.rollback = MagicMock()
|
|
mock_session.close = MagicMock()
|
|
|
|
with (
|
|
patch("app.tasks.background_tasks.CSVProcessor") as mock_processor,
|
|
patch(
|
|
"app.tasks.background_tasks.SessionLocal", return_value=mock_session
|
|
),
|
|
):
|
|
mock_instance = mock_processor.return_value
|
|
mock_instance.process_marketplace_csv_from_url = AsyncMock(
|
|
side_effect=Exception("Import failed")
|
|
)
|
|
|
|
# Run background task - should handle commit error gracefully
|
|
await process_marketplace_import(
|
|
job_id,
|
|
"http://example.com/test.csv",
|
|
"TestMarket",
|
|
test_vendor.id,
|
|
1000,
|
|
)
|
|
|
|
# Verify rollback was called after commit failure
|
|
mock_session.rollback.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_marketplace_import_close_error_handling(
|
|
self, db, test_user, test_vendor
|
|
):
|
|
"""Test handling when session close fails"""
|
|
# Create import job
|
|
job = MarketplaceImportJob(
|
|
status="pending",
|
|
source_url="http://example.com/test.csv",
|
|
marketplace="TestMarket",
|
|
vendor_id=test_vendor.id,
|
|
user_id=test_user.id,
|
|
language="en",
|
|
)
|
|
db.add(job)
|
|
db.commit()
|
|
db.refresh(job)
|
|
|
|
# Store the job ID before it becomes detached
|
|
job_id = job.id
|
|
|
|
# Create a mock session that will fail on close
|
|
mock_session = MagicMock()
|
|
mock_session.query.return_value.filter.return_value.first.side_effect = [
|
|
job, # First call returns the job
|
|
test_vendor, # Second call returns the vendor
|
|
]
|
|
mock_session.commit = MagicMock()
|
|
mock_session.close.side_effect = Exception("Simulated close failure")
|
|
|
|
with (
|
|
patch("app.tasks.background_tasks.CSVProcessor") as mock_processor,
|
|
patch(
|
|
"app.tasks.background_tasks.SessionLocal", return_value=mock_session
|
|
),
|
|
):
|
|
mock_instance = mock_processor.return_value
|
|
mock_instance.process_marketplace_csv_from_url = AsyncMock(
|
|
return_value={
|
|
"imported": 10,
|
|
"updated": 5,
|
|
"total_processed": 15,
|
|
"errors": 0,
|
|
}
|
|
)
|
|
|
|
# Run background task - should handle close error gracefully
|
|
await process_marketplace_import(
|
|
job_id,
|
|
"http://example.com/test.csv",
|
|
"TestMarket",
|
|
test_vendor.id,
|
|
1000,
|
|
)
|
|
|
|
# The close method should have been called
|
|
mock_session.close.assert_called_once()
|