Marketplace tests update
This commit is contained in:
@@ -113,7 +113,7 @@ def delete_product(
|
|||||||
return {"message": "Product and associated stock deleted successfully"}
|
return {"message": "Product and associated stock deleted successfully"}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/export-csv")
|
@router.get("/product/export-csv")
|
||||||
async def export_csv(
|
async def export_csv(
|
||||||
marketplace: Optional[str] = Query(None, description="Filter by marketplace"),
|
marketplace: Optional[str] = Query(None, description="Filter by marketplace"),
|
||||||
shop_name: Optional[str] = Query(None, description="Filter by shop name"),
|
shop_name: Optional[str] = Query(None, description="Filter by shop name"),
|
||||||
|
|||||||
@@ -85,6 +85,24 @@ def setup_exception_handlers(app):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Clean up validation errors to ensure JSON serializability
|
||||||
|
clean_errors = []
|
||||||
|
for error in exc.errors():
|
||||||
|
clean_error = {}
|
||||||
|
for key, value in error.items():
|
||||||
|
if key == 'ctx' and isinstance(value, dict):
|
||||||
|
# Handle the 'ctx' field that contains ValueError objects
|
||||||
|
clean_ctx = {}
|
||||||
|
for ctx_key, ctx_value in value.items():
|
||||||
|
if isinstance(ctx_value, Exception):
|
||||||
|
clean_ctx[ctx_key] = str(ctx_value) # Convert exception to string
|
||||||
|
else:
|
||||||
|
clean_ctx[ctx_key] = ctx_value
|
||||||
|
clean_error[key] = clean_ctx
|
||||||
|
else:
|
||||||
|
clean_error[key] = value
|
||||||
|
clean_errors.append(clean_error)
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=422,
|
status_code=422,
|
||||||
content={
|
content={
|
||||||
@@ -92,7 +110,7 @@ def setup_exception_handlers(app):
|
|||||||
"message": "Request validation failed",
|
"message": "Request validation failed",
|
||||||
"status_code": 422,
|
"status_code": 422,
|
||||||
"details": {
|
"details": {
|
||||||
"validation_errors": exc.errors()
|
"validation_errors": clean_errors # Use cleaned errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ This module provides classes and functions for:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional, Tuple
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -81,7 +81,7 @@ class AdminService:
|
|||||||
try:
|
try:
|
||||||
original_status = user.is_active
|
original_status = user.is_active
|
||||||
user.is_active = not user.is_active
|
user.is_active = not user.is_active
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(timezone.utc)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(user)
|
db.refresh(user)
|
||||||
|
|
||||||
@@ -146,11 +146,11 @@ class AdminService:
|
|||||||
try:
|
try:
|
||||||
original_status = shop.is_verified
|
original_status = shop.is_verified
|
||||||
shop.is_verified = not shop.is_verified
|
shop.is_verified = not shop.is_verified
|
||||||
shop.updated_at = datetime.utcnow()
|
shop.updated_at = datetime.now(timezone.utc)
|
||||||
|
|
||||||
# Add verification timestamp if implementing audit trail
|
# Add verification timestamp if implementing audit trail
|
||||||
if shop.is_verified:
|
if shop.is_verified:
|
||||||
shop.verified_at = datetime.utcnow()
|
shop.verified_at = datetime.now(timezone.utc)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(shop)
|
db.refresh(shop)
|
||||||
@@ -190,7 +190,7 @@ class AdminService:
|
|||||||
try:
|
try:
|
||||||
original_status = shop.is_active
|
original_status = shop.is_active
|
||||||
shop.is_active = not shop.is_active
|
shop.is_active = not shop.is_active
|
||||||
shop.updated_at = datetime.utcnow()
|
shop.updated_at = datetime.now(timezone.utc)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(shop)
|
db.refresh(shop)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ This module provides classes and functions for:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
@@ -106,7 +106,7 @@ class MarketplaceService:
|
|||||||
shop_id=shop.id, # Foreign key to shops table
|
shop_id=shop.id, # Foreign key to shops table
|
||||||
shop_name=shop.shop_name, # Use shop.shop_name (the display name)
|
shop_name=shop.shop_name, # Use shop.shop_name (the display name)
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
created_at=datetime.utcnow(),
|
created_at=datetime.now(timezone.utc),
|
||||||
)
|
)
|
||||||
|
|
||||||
db.add(import_job)
|
db.add(import_job)
|
||||||
@@ -360,7 +360,7 @@ class MarketplaceService:
|
|||||||
raise ImportJobCannotBeCancelledException(job_id, job.status)
|
raise ImportJobCannotBeCancelledException(job_id, job.status)
|
||||||
|
|
||||||
job.status = "cancelled"
|
job.status = "cancelled"
|
||||||
job.completed_at = datetime.utcnow()
|
job.completed_at = datetime.now(timezone.utc)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(job)
|
db.refresh(job)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ This module provides classes and functions for:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from typing import Generator, List, Optional, Tuple
|
from typing import Generator, List, Optional, Tuple
|
||||||
|
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
@@ -250,7 +250,7 @@ class ProductService:
|
|||||||
for key, value in update_data.items():
|
for key, value in update_data.items():
|
||||||
setattr(product, key, value)
|
setattr(product, key, value)
|
||||||
|
|
||||||
product.updated_at = datetime.utcnow()
|
product.updated_at = datetime.now(timezone.utc)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(product)
|
db.refresh(product)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ This module provides classes and functions for:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -68,7 +68,7 @@ class StockService:
|
|||||||
# Update existing stock (SET to exact quantity)
|
# Update existing stock (SET to exact quantity)
|
||||||
old_quantity = existing_stock.quantity
|
old_quantity = existing_stock.quantity
|
||||||
existing_stock.quantity = stock_data.quantity
|
existing_stock.quantity = stock_data.quantity
|
||||||
existing_stock.updated_at = datetime.utcnow()
|
existing_stock.updated_at = datetime.now(timezone.utc)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(existing_stock)
|
db.refresh(existing_stock)
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ class StockService:
|
|||||||
# Add to existing stock
|
# Add to existing stock
|
||||||
old_quantity = existing_stock.quantity
|
old_quantity = existing_stock.quantity
|
||||||
existing_stock.quantity += stock_data.quantity
|
existing_stock.quantity += stock_data.quantity
|
||||||
existing_stock.updated_at = datetime.utcnow()
|
existing_stock.updated_at = datetime.now(timezone.utc)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(existing_stock)
|
db.refresh(existing_stock)
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ class StockService:
|
|||||||
raise NegativeStockException(normalized_gtin, location, new_quantity)
|
raise NegativeStockException(normalized_gtin, location, new_quantity)
|
||||||
|
|
||||||
existing_stock.quantity = new_quantity
|
existing_stock.quantity = new_quantity
|
||||||
existing_stock.updated_at = datetime.utcnow()
|
existing_stock.updated_at = datetime.now(timezone.utc)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(existing_stock)
|
db.refresh(existing_stock)
|
||||||
|
|
||||||
@@ -381,7 +381,7 @@ class StockService:
|
|||||||
self._validate_quantity(stock_update.quantity, allow_zero=True)
|
self._validate_quantity(stock_update.quantity, allow_zero=True)
|
||||||
|
|
||||||
stock_entry.quantity = stock_update.quantity
|
stock_entry.quantity = stock_update.quantity
|
||||||
stock_entry.updated_at = datetime.utcnow()
|
stock_entry.updated_at = datetime.now(timezone.utc)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(stock_entry)
|
db.refresh(stock_entry)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ This module provides classes and functions for:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from app.core.database import SessionLocal
|
from app.core.database import SessionLocal
|
||||||
from models.database.marketplace import MarketplaceImportJob
|
from models.database.marketplace import MarketplaceImportJob
|
||||||
@@ -37,7 +37,7 @@ async def process_marketplace_import(
|
|||||||
return
|
return
|
||||||
|
|
||||||
job.status = "processing"
|
job.status = "processing"
|
||||||
job.started_at = datetime.utcnow()
|
job.started_at = datetime.now(timezone.utc)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
logger.info(f"Processing import: Job {job_id}, Marketplace: {marketplace}")
|
logger.info(f"Processing import: Job {job_id}, Marketplace: {marketplace}")
|
||||||
@@ -49,7 +49,7 @@ async def process_marketplace_import(
|
|||||||
|
|
||||||
# Update job with results
|
# Update job with results
|
||||||
job.status = "completed"
|
job.status = "completed"
|
||||||
job.completed_at = datetime.utcnow()
|
job.completed_at = datetime.now(timezone.utc)
|
||||||
job.imported_count = result["imported"]
|
job.imported_count = result["imported"]
|
||||||
job.updated_count = result["updated"]
|
job.updated_count = result["updated"]
|
||||||
job.error_count = result.get("errors", 0)
|
job.error_count = result.get("errors", 0)
|
||||||
@@ -68,7 +68,7 @@ async def process_marketplace_import(
|
|||||||
try:
|
try:
|
||||||
job.status = "failed"
|
job.status = "failed"
|
||||||
job.error_message = str(e)
|
job.error_message = str(e)
|
||||||
job.completed_at = datetime.utcnow()
|
job.completed_at = datetime.now(timezone.utc)
|
||||||
db.commit()
|
db.commit()
|
||||||
except Exception as commit_error:
|
except Exception as commit_error:
|
||||||
logger.error(f"Failed to update job status: {commit_error}")
|
logger.error(f"Failed to update job status: {commit_error}")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ This module provides classes and functions for:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
@@ -290,7 +290,7 @@ class CSVProcessor:
|
|||||||
existing_product, key
|
existing_product, key
|
||||||
):
|
):
|
||||||
setattr(existing_product, key, value)
|
setattr(existing_product, key, value)
|
||||||
existing_product.updated_at = datetime.utcnow()
|
existing_product.updated_at = datetime.now(timezone.utc)
|
||||||
updated += 1
|
updated += 1
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Updated product {product_data['product_id']} for "
|
f"Updated product {product_data['product_id']} for "
|
||||||
|
|||||||
4
main.py
4
main.py
@@ -1,5 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from fastapi import Depends, FastAPI, HTTPException
|
from fastapi import Depends, FastAPI, HTTPException
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
@@ -56,7 +56,7 @@ def health_check(db: Session = Depends(get_db)):
|
|||||||
db.execute(text("SELECT 1"))
|
db.execute(text("SELECT 1"))
|
||||||
return {
|
return {
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"timestamp": datetime.utcnow(),
|
"timestamp": datetime.now(timezone.utc),
|
||||||
"message": f"{settings.project_name} v{settings.version}",
|
"message": f"{settings.project_name} v{settings.version}",
|
||||||
"docs": {
|
"docs": {
|
||||||
"swagger": "/docs",
|
"swagger": "/docs",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ This module provides classes and functions for:
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
@@ -71,7 +71,7 @@ class AuthManager:
|
|||||||
def create_access_token(self, user: User) -> Dict[str, Any]:
|
def create_access_token(self, user: User) -> Dict[str, Any]:
|
||||||
"""Create JWT access token for user."""
|
"""Create JWT access token for user."""
|
||||||
expires_delta = timedelta(minutes=self.token_expire_minutes)
|
expires_delta = timedelta(minutes=self.token_expire_minutes)
|
||||||
expire = datetime.utcnow() + expires_delta
|
expire = datetime.now(timezone.utc) + expires_delta
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"sub": str(user.id),
|
"sub": str(user.id),
|
||||||
@@ -79,7 +79,7 @@ class AuthManager:
|
|||||||
"email": user.email,
|
"email": user.email,
|
||||||
"role": user.role,
|
"role": user.role,
|
||||||
"exp": expire,
|
"exp": expire,
|
||||||
"iat": datetime.utcnow(),
|
"iat": datetime.now(timezone.utc),
|
||||||
}
|
}
|
||||||
|
|
||||||
token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
|
token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
|
||||||
@@ -100,7 +100,7 @@ class AuthManager:
|
|||||||
if exp is None:
|
if exp is None:
|
||||||
raise InvalidTokenException("Token missing expiration")
|
raise InvalidTokenException("Token missing expiration")
|
||||||
|
|
||||||
if datetime.utcnow() > datetime.fromtimestamp(exp):
|
if datetime.now(timezone.utc) > datetime.fromtimestamp(exp):
|
||||||
raise TokenExpiredException()
|
raise TokenExpiredException()
|
||||||
|
|
||||||
# Extract user data
|
# Extract user data
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ This module provides classes and functions for:
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -23,7 +23,7 @@ class RateLimiter:
|
|||||||
# Dictionary to store request timestamps for each client
|
# Dictionary to store request timestamps for each client
|
||||||
self.clients: Dict[str, deque] = defaultdict(lambda: deque())
|
self.clients: Dict[str, deque] = defaultdict(lambda: deque())
|
||||||
self.cleanup_interval = 3600 # Clean up old entries every hour
|
self.cleanup_interval = 3600 # Clean up old entries every hour
|
||||||
self.last_cleanup = datetime.utcnow()
|
self.last_cleanup = datetime.now(timezone.utc)
|
||||||
|
|
||||||
def allow_request(
|
def allow_request(
|
||||||
self, client_id: str, max_requests: int, window_seconds: int
|
self, client_id: str, max_requests: int, window_seconds: int
|
||||||
@@ -33,7 +33,7 @@ class RateLimiter:
|
|||||||
|
|
||||||
Uses sliding window algorithm
|
Uses sliding window algorithm
|
||||||
"""
|
"""
|
||||||
now = datetime.utcnow()
|
now = datetime.now(timezone.utc)
|
||||||
window_start = now - timedelta(seconds=window_seconds)
|
window_start = now - timedelta(seconds=window_seconds)
|
||||||
|
|
||||||
# Clean up old entries periodically
|
# Clean up old entries periodically
|
||||||
@@ -60,7 +60,7 @@ class RateLimiter:
|
|||||||
|
|
||||||
def _cleanup_old_entries(self):
|
def _cleanup_old_entries(self):
|
||||||
"""Clean up old entries to prevent memory leaks."""
|
"""Clean up old entries to prevent memory leaks."""
|
||||||
cutoff_time = datetime.utcnow() - timedelta(hours=24)
|
cutoff_time = datetime.now(timezone.utc) - timedelta(hours=24)
|
||||||
|
|
||||||
clients_to_remove = []
|
clients_to_remove = []
|
||||||
for client_id, requests in self.clients.items():
|
for client_id, requests in self.clients.items():
|
||||||
@@ -84,7 +84,7 @@ class RateLimiter:
|
|||||||
"""Get statistics for a specific client."""
|
"""Get statistics for a specific client."""
|
||||||
client_requests = self.clients.get(client_id, deque())
|
client_requests = self.clients.get(client_id, deque())
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.now(timezone.utc)
|
||||||
hour_ago = now - timedelta(hours=1)
|
hour_ago = now - timedelta(hours=1)
|
||||||
day_ago = now - timedelta(days=1)
|
day_ago = now - timedelta(days=1)
|
||||||
|
|
||||||
|
|||||||
6
tests/fixtures/shop_fixtures.py
vendored
6
tests/fixtures/shop_fixtures.py
vendored
@@ -10,10 +10,10 @@ from models.database.stock import Stock
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_shop(db, test_user):
|
def test_shop(db, test_user):
|
||||||
"""Create a test shop with unique shop code"""
|
"""Create a test shop with unique shop code"""
|
||||||
unique_id = str(uuid.uuid4())[:8] # Short unique identifier
|
unique_id = str(uuid.uuid4())[:8].upper() # Make unique ID uppercase
|
||||||
shop = Shop(
|
shop = Shop(
|
||||||
shop_code=f"TESTSHOP_{unique_id}",
|
shop_code=f"TESTSHOP_{unique_id}", # Will be all uppercase
|
||||||
shop_name=f"Test Shop {unique_id}",
|
shop_name=f"Test Shop {unique_id.lower()}", # Keep display name readable
|
||||||
owner_id=test_user.id,
|
owner_id=test_user.id,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
is_verified=True,
|
is_verified=True,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# tests/integration/api/v1/test_auth_endpoints.py
|
# tests/integration/api/v1/test_auth_endpoints.py
|
||||||
import pytest
|
import pytest
|
||||||
from jose import jwt
|
from jose import jwt
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
@pytest.mark.api
|
@pytest.mark.api
|
||||||
@@ -195,8 +196,8 @@ class TestAuthenticationAPI:
|
|||||||
"username": test_user.username,
|
"username": test_user.username,
|
||||||
"email": test_user.email,
|
"email": test_user.email,
|
||||||
"role": test_user.role,
|
"role": test_user.role,
|
||||||
"exp": datetime.utcnow() - timedelta(hours=1),
|
"exp": datetime.now(timezone.utc) - timedelta(hours=1),
|
||||||
"iat": datetime.utcnow() - timedelta(hours=2),
|
"iat": datetime.now(timezone.utc) - timedelta(hours=2),
|
||||||
}
|
}
|
||||||
|
|
||||||
expired_token = jwt.encode(
|
expired_token = jwt.encode(
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from models.database.product import Product
|
|||||||
class TestExportFunctionality:
|
class TestExportFunctionality:
|
||||||
def test_csv_export_basic(self, client, auth_headers, test_product):
|
def test_csv_export_basic(self, client, auth_headers, test_product):
|
||||||
"""Test basic CSV export functionality"""
|
"""Test basic CSV export functionality"""
|
||||||
response = client.get("/api/v1/export-csv", headers=auth_headers)
|
response = client.get("/api/v1/product/export-csv", headers=auth_headers)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.headers["content-type"] == "text/csv; charset=utf-8"
|
assert response.headers["content-type"] == "text/csv; charset=utf-8"
|
||||||
@@ -40,7 +40,7 @@ class TestExportFunctionality:
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
"/api/v1/export-csv?marketplace=Amazon", headers=auth_headers
|
"/api/v1/product/export-csv?marketplace=Amazon", headers=auth_headers
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ class TestExportFunctionality:
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
response = client.get("/api/v1/export-csv", headers=auth_headers)
|
response = client.get("/api/v1/product/export-csv", headers=auth_headers)
|
||||||
end_time = time.time()
|
end_time = time.time()
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|||||||
@@ -4,9 +4,14 @@ from unittest.mock import AsyncMock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
@pytest.mark.api
|
||||||
|
@pytest.mark.marketplace
|
||||||
class TestMarketplaceAPI:
|
class TestMarketplaceAPI:
|
||||||
def test_import_from_marketplace(self, client, auth_headers, test_shop):
|
def test_import_from_marketplace(self, client, auth_headers, test_shop, test_user):
|
||||||
"""Test marketplace import endpoint - just test job creation"""
|
"""Test marketplace import endpoint - just test job creation"""
|
||||||
|
# Ensure user owns the shop
|
||||||
|
test_shop.owner_id = test_user.id
|
||||||
|
|
||||||
import_data = {
|
import_data = {
|
||||||
"url": "https://example.com/products.csv",
|
"url": "https://example.com/products.csv",
|
||||||
@@ -23,8 +28,8 @@ class TestMarketplaceAPI:
|
|||||||
assert data["status"] == "pending"
|
assert data["status"] == "pending"
|
||||||
assert data["marketplace"] == "TestMarket"
|
assert data["marketplace"] == "TestMarket"
|
||||||
assert "job_id" in data
|
assert "job_id" in data
|
||||||
|
assert data["shop_code"] == test_shop.shop_code
|
||||||
# Don't test the background task here - test it separately
|
assert data["shop_id"] == test_shop.id
|
||||||
|
|
||||||
def test_import_from_marketplace_invalid_shop(self, client, auth_headers):
|
def test_import_from_marketplace_invalid_shop(self, client, auth_headers):
|
||||||
"""Test marketplace import with invalid shop"""
|
"""Test marketplace import with invalid shop"""
|
||||||
@@ -39,16 +44,383 @@ class TestMarketplaceAPI:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
assert "Shop not found" in response.json()["detail"]
|
data = response.json()
|
||||||
|
assert data["error_code"] == "SHOP_NOT_FOUND"
|
||||||
|
assert "NONEXISTENT" in data["message"]
|
||||||
|
|
||||||
def test_get_marketplace_import_jobs(self, client, auth_headers):
|
def test_import_from_marketplace_unauthorized_shop(self, client, auth_headers, test_shop, other_user):
|
||||||
|
"""Test marketplace import with unauthorized shop access"""
|
||||||
|
# Set shop owner to different user
|
||||||
|
test_shop.owner_id = other_user.id
|
||||||
|
|
||||||
|
import_data = {
|
||||||
|
"url": "https://example.com/products.csv",
|
||||||
|
"marketplace": "TestMarket",
|
||||||
|
"shop_code": test_shop.shop_code,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1/marketplace/import-product", headers=auth_headers, json=import_data
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
data = response.json()
|
||||||
|
assert data["error_code"] == "UNAUTHORIZED_SHOP_ACCESS"
|
||||||
|
assert test_shop.shop_code in data["message"]
|
||||||
|
|
||||||
|
def test_import_from_marketplace_validation_error(self, client, auth_headers):
|
||||||
|
"""Test marketplace import with invalid request data"""
|
||||||
|
import_data = {
|
||||||
|
"url": "", # Empty URL
|
||||||
|
"marketplace": "", # Empty marketplace
|
||||||
|
# Missing shop_code
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1/marketplace/import-product", headers=auth_headers, json=import_data
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 422
|
||||||
|
data = response.json()
|
||||||
|
assert data["error_code"] == "VALIDATION_ERROR"
|
||||||
|
assert "Request validation failed" in data["message"]
|
||||||
|
|
||||||
|
def test_import_from_marketplace_admin_access(self, client, admin_headers, test_shop):
|
||||||
|
"""Test that admin can import for any shop"""
|
||||||
|
import_data = {
|
||||||
|
"url": "https://example.com/products.csv",
|
||||||
|
"marketplace": "AdminMarket",
|
||||||
|
"shop_code": test_shop.shop_code,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1/marketplace/import-product", headers=admin_headers, json=import_data
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["marketplace"] == "AdminMarket"
|
||||||
|
assert data["shop_code"] == test_shop.shop_code
|
||||||
|
|
||||||
|
def test_get_marketplace_import_status(self, client, auth_headers, test_marketplace_job):
|
||||||
|
"""Test getting marketplace import status"""
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1/marketplace/import-status/{test_marketplace_job.id}",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["job_id"] == test_marketplace_job.id
|
||||||
|
assert data["status"] == test_marketplace_job.status
|
||||||
|
assert data["marketplace"] == test_marketplace_job.marketplace
|
||||||
|
|
||||||
|
def test_get_marketplace_import_status_not_found(self, client, auth_headers):
|
||||||
|
"""Test getting status of non-existent import job"""
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1/marketplace/import-status/99999",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
data = response.json()
|
||||||
|
assert data["error_code"] == "IMPORT_JOB_NOT_FOUND"
|
||||||
|
assert "99999" in data["message"]
|
||||||
|
|
||||||
|
def test_get_marketplace_import_status_unauthorized(self, client, auth_headers, test_marketplace_job, other_user):
|
||||||
|
"""Test getting status of unauthorized import job"""
|
||||||
|
# Change job owner to other user
|
||||||
|
test_marketplace_job.user_id = other_user.id
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1/marketplace/import-status/{test_marketplace_job.id}",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
data = response.json()
|
||||||
|
assert data["error_code"] == "IMPORT_JOB_NOT_OWNED"
|
||||||
|
|
||||||
|
def test_get_marketplace_import_jobs(self, client, auth_headers, test_marketplace_job):
|
||||||
"""Test getting marketplace import jobs"""
|
"""Test getting marketplace import jobs"""
|
||||||
response = client.get("/api/v1/marketplace/import-jobs", headers=auth_headers)
|
response = client.get("/api/v1/marketplace/import-jobs", headers=auth_headers)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert isinstance(response.json(), list)
|
data = response.json()
|
||||||
|
assert isinstance(data, list)
|
||||||
|
assert len(data) >= 1
|
||||||
|
|
||||||
|
# Find our test job in the results
|
||||||
|
job_ids = [job["job_id"] for job in data]
|
||||||
|
assert test_marketplace_job.id in job_ids
|
||||||
|
|
||||||
|
def test_get_marketplace_import_jobs_with_filters(self, client, auth_headers, test_marketplace_job):
|
||||||
|
"""Test getting import jobs with filters"""
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1/marketplace/import-jobs?marketplace={test_marketplace_job.marketplace}",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert isinstance(data, list)
|
||||||
|
assert len(data) >= 1
|
||||||
|
|
||||||
|
for job in data:
|
||||||
|
assert test_marketplace_job.marketplace.lower() in job["marketplace"].lower()
|
||||||
|
|
||||||
|
def test_get_marketplace_import_jobs_pagination(self, client, auth_headers):
|
||||||
|
"""Test import jobs pagination"""
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1/marketplace/import-jobs?skip=0&limit=5",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert isinstance(data, list)
|
||||||
|
assert len(data) <= 5
|
||||||
|
|
||||||
|
def test_get_marketplace_import_stats(self, client, auth_headers, test_marketplace_job):
|
||||||
|
"""Test getting marketplace import statistics"""
|
||||||
|
response = client.get("/api/v1/marketplace/marketplace-import-stats", headers=auth_headers)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "total_jobs" in data
|
||||||
|
assert "pending_jobs" in data
|
||||||
|
assert "running_jobs" in data
|
||||||
|
assert "completed_jobs" in data
|
||||||
|
assert "failed_jobs" in data
|
||||||
|
assert isinstance(data["total_jobs"], int)
|
||||||
|
assert data["total_jobs"] >= 1
|
||||||
|
|
||||||
|
def test_cancel_marketplace_import_job(self, client, auth_headers, test_user, test_shop, db):
|
||||||
|
"""Test cancelling a marketplace import job"""
|
||||||
|
# Create a pending job that can be cancelled
|
||||||
|
from models.database.marketplace import MarketplaceImportJob
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
|
job = MarketplaceImportJob(
|
||||||
|
status="pending",
|
||||||
|
marketplace="TestMarket",
|
||||||
|
shop_name=f"Test_Shop_{unique_id}",
|
||||||
|
user_id=test_user.id,
|
||||||
|
shop_id=test_shop.id,
|
||||||
|
source_url="https://test.example.com/import",
|
||||||
|
imported_count=0,
|
||||||
|
updated_count=0,
|
||||||
|
total_processed=0,
|
||||||
|
error_count=0,
|
||||||
|
)
|
||||||
|
db.add(job)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(job)
|
||||||
|
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1/marketplace/import-jobs/{job.id}/cancel",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["job_id"] == job.id
|
||||||
|
assert data["status"] == "cancelled"
|
||||||
|
assert data["completed_at"] is not None
|
||||||
|
|
||||||
|
def test_cancel_marketplace_import_job_not_found(self, client, auth_headers):
|
||||||
|
"""Test cancelling non-existent import job"""
|
||||||
|
response = client.put(
|
||||||
|
"/api/v1/marketplace/import-jobs/99999/cancel",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
data = response.json()
|
||||||
|
assert data["error_code"] == "IMPORT_JOB_NOT_FOUND"
|
||||||
|
|
||||||
|
def test_cancel_marketplace_import_job_cannot_cancel(self, client, auth_headers, test_marketplace_job, db):
|
||||||
|
"""Test cancelling a job that cannot be cancelled"""
|
||||||
|
# Set job to completed status
|
||||||
|
test_marketplace_job.status = "completed"
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1/marketplace/import-jobs/{test_marketplace_job.id}/cancel",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
data = response.json()
|
||||||
|
assert data["error_code"] == "IMPORT_JOB_CANNOT_BE_CANCELLED"
|
||||||
|
assert "completed" in data["message"]
|
||||||
|
|
||||||
|
def test_delete_marketplace_import_job(self, client, auth_headers, test_user, test_shop, db):
|
||||||
|
"""Test deleting a marketplace import job"""
|
||||||
|
# Create a completed job that can be deleted
|
||||||
|
from models.database.marketplace import MarketplaceImportJob
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
|
job = MarketplaceImportJob(
|
||||||
|
status="completed",
|
||||||
|
marketplace="TestMarket",
|
||||||
|
shop_name=f"Test_Shop_{unique_id}",
|
||||||
|
user_id=test_user.id,
|
||||||
|
shop_id=test_shop.id,
|
||||||
|
source_url="https://test.example.com/import",
|
||||||
|
imported_count=0,
|
||||||
|
updated_count=0,
|
||||||
|
total_processed=0,
|
||||||
|
error_count=0,
|
||||||
|
)
|
||||||
|
db.add(job)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(job)
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1/marketplace/import-jobs/{job.id}",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "deleted successfully" in data["message"]
|
||||||
|
|
||||||
|
def test_delete_marketplace_import_job_not_found(self, client, auth_headers):
|
||||||
|
"""Test deleting non-existent import job"""
|
||||||
|
response = client.delete(
|
||||||
|
"/api/v1/marketplace/import-jobs/99999",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
data = response.json()
|
||||||
|
assert data["error_code"] == "IMPORT_JOB_NOT_FOUND"
|
||||||
|
|
||||||
|
def test_delete_marketplace_import_job_cannot_delete(self, client, auth_headers, test_user, test_shop, db):
|
||||||
|
"""Test deleting a job that cannot be deleted"""
|
||||||
|
# Create a pending job that cannot be deleted
|
||||||
|
from models.database.marketplace import MarketplaceImportJob
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
|
job = MarketplaceImportJob(
|
||||||
|
status="pending",
|
||||||
|
marketplace="TestMarket",
|
||||||
|
shop_name=f"Test_Shop_{unique_id}",
|
||||||
|
user_id=test_user.id,
|
||||||
|
shop_id=test_shop.id,
|
||||||
|
source_url="https://test.example.com/import",
|
||||||
|
imported_count=0,
|
||||||
|
updated_count=0,
|
||||||
|
total_processed=0,
|
||||||
|
error_count=0,
|
||||||
|
)
|
||||||
|
db.add(job)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(job)
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1/marketplace/import-jobs/{job.id}",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
data = response.json()
|
||||||
|
assert data["error_code"] == "IMPORT_JOB_CANNOT_BE_DELETED"
|
||||||
|
assert "pending" in data["message"]
|
||||||
|
|
||||||
def test_get_marketplace_without_auth(self, client):
|
def test_get_marketplace_without_auth(self, client):
|
||||||
"""Test that marketplace endpoints require authentication"""
|
"""Test that marketplace endpoints require authentication"""
|
||||||
response = client.get("/api/v1/marketplace/import-jobs")
|
response = client.get("/api/v1/marketplace/import-jobs")
|
||||||
assert response.status_code == 401 # No authorization header
|
assert response.status_code == 401
|
||||||
|
data = response.json()
|
||||||
|
assert data["error_code"] == "INVALID_TOKEN"
|
||||||
|
|
||||||
|
def test_import_without_auth(self, client):
|
||||||
|
"""Test marketplace import without authentication"""
|
||||||
|
import_data = {
|
||||||
|
"url": "https://example.com/products.csv",
|
||||||
|
"marketplace": "TestMarket",
|
||||||
|
"shop_code": "TEST_SHOP",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post("/api/v1/marketplace/import-product", json=import_data)
|
||||||
|
assert response.status_code == 401
|
||||||
|
data = response.json()
|
||||||
|
assert data["error_code"] == "INVALID_TOKEN"
|
||||||
|
|
||||||
|
def test_admin_can_access_all_jobs(self, client, admin_headers, test_marketplace_job):
|
||||||
|
"""Test that admin can access all import jobs"""
|
||||||
|
response = client.get("/api/v1/marketplace/import-jobs", headers=admin_headers)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert isinstance(data, list)
|
||||||
|
# Admin should see all jobs, including the test job
|
||||||
|
job_ids = [job["job_id"] for job in data]
|
||||||
|
assert test_marketplace_job.id in job_ids
|
||||||
|
|
||||||
|
def test_admin_can_view_any_job_status(self, client, admin_headers, test_marketplace_job):
|
||||||
|
"""Test that admin can view any job status"""
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1/marketplace/import-status/{test_marketplace_job.id}",
|
||||||
|
headers=admin_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["job_id"] == test_marketplace_job.id
|
||||||
|
|
||||||
|
def test_admin_can_cancel_any_job(self, client, admin_headers, test_user, test_shop, db):
|
||||||
|
"""Test that admin can cancel any job"""
|
||||||
|
# Create a pending job owned by different user
|
||||||
|
from models.database.marketplace import MarketplaceImportJob
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
|
job = MarketplaceImportJob(
|
||||||
|
status="pending",
|
||||||
|
marketplace="TestMarket",
|
||||||
|
shop_name=f"Test_Shop_{unique_id}",
|
||||||
|
user_id=test_user.id, # Different user
|
||||||
|
shop_id=test_shop.id,
|
||||||
|
source_url="https://test.example.com/import",
|
||||||
|
imported_count=0,
|
||||||
|
updated_count=0,
|
||||||
|
total_processed=0,
|
||||||
|
error_count=0,
|
||||||
|
)
|
||||||
|
db.add(job)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(job)
|
||||||
|
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1/marketplace/import-jobs/{job.id}/cancel",
|
||||||
|
headers=admin_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["status"] == "cancelled"
|
||||||
|
|
||||||
|
def test_rate_limiting_applied(self, client, auth_headers, test_shop, test_user):
|
||||||
|
"""Test that rate limiting is applied to import endpoint"""
|
||||||
|
# This test verifies that the rate_limit decorator is present
|
||||||
|
# Actual rate limiting testing would require multiple requests
|
||||||
|
test_shop.owner_id = test_user.id
|
||||||
|
|
||||||
|
import_data = {
|
||||||
|
"url": "https://example.com/products.csv",
|
||||||
|
"marketplace": "TestMarket",
|
||||||
|
"shop_code": test_shop.shop_code,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1/marketplace/import-product", headers=auth_headers, json=import_data
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should succeed on first request
|
||||||
|
assert response.status_code == 200
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ from datetime import datetime
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from app.exceptions.marketplace import (
|
||||||
|
ImportJobNotFoundException,
|
||||||
|
ImportJobNotOwnedException,
|
||||||
|
ImportJobCannotBeCancelledException,
|
||||||
|
ImportJobCannotBeDeletedException,
|
||||||
|
)
|
||||||
|
from app.exceptions.shop import ShopNotFoundException, UnauthorizedShopAccessException
|
||||||
|
from app.exceptions.base import ValidationException
|
||||||
from app.services.marketplace_service import MarketplaceService
|
from app.services.marketplace_service import MarketplaceService
|
||||||
from models.schemas.marketplace import MarketplaceImportRequest
|
from models.schemas.marketplace import MarketplaceImportRequest
|
||||||
from models.database.marketplace import MarketplaceImportJob
|
from models.database.marketplace import MarketplaceImportJob
|
||||||
@@ -38,9 +46,14 @@ class TestMarketplaceService:
|
|||||||
|
|
||||||
def test_validate_shop_access_shop_not_found(self, db, test_user):
|
def test_validate_shop_access_shop_not_found(self, db, test_user):
|
||||||
"""Test shop access validation when shop doesn't exist"""
|
"""Test shop access validation when shop doesn't exist"""
|
||||||
with pytest.raises(ValueError, match="Shop not found"):
|
with pytest.raises(ShopNotFoundException) as exc_info:
|
||||||
self.service.validate_shop_access(db, "NONEXISTENT", test_user)
|
self.service.validate_shop_access(db, "NONEXISTENT", test_user)
|
||||||
|
|
||||||
|
exception = exc_info.value
|
||||||
|
assert exception.error_code == "SHOP_NOT_FOUND"
|
||||||
|
assert exception.status_code == 404
|
||||||
|
assert "NONEXISTENT" in exception.message
|
||||||
|
|
||||||
def test_validate_shop_access_permission_denied(
|
def test_validate_shop_access_permission_denied(
|
||||||
self, db, test_shop, test_user, other_user
|
self, db, test_shop, test_user, other_user
|
||||||
):
|
):
|
||||||
@@ -49,9 +62,14 @@ class TestMarketplaceService:
|
|||||||
test_shop.owner_id = other_user.id
|
test_shop.owner_id = other_user.id
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
with pytest.raises(PermissionError, match="Access denied to this shop"):
|
with pytest.raises(UnauthorizedShopAccessException) as exc_info:
|
||||||
self.service.validate_shop_access(db, test_shop.shop_code, test_user)
|
self.service.validate_shop_access(db, test_shop.shop_code, test_user)
|
||||||
|
|
||||||
|
exception = exc_info.value
|
||||||
|
assert exception.error_code == "UNAUTHORIZED_SHOP_ACCESS"
|
||||||
|
assert exception.status_code == 403
|
||||||
|
assert test_shop.shop_code in exception.message
|
||||||
|
|
||||||
def test_create_import_job_success(self, db, test_shop, test_user):
|
def test_create_import_job_success(self, db, test_shop, test_user):
|
||||||
"""Test successful creation of import job"""
|
"""Test successful creation of import job"""
|
||||||
# Set the shop owner to the test user
|
# Set the shop owner to the test user
|
||||||
@@ -68,11 +86,11 @@ class TestMarketplaceService:
|
|||||||
result = self.service.create_import_job(db, request, test_user)
|
result = self.service.create_import_job(db, request, test_user)
|
||||||
|
|
||||||
assert result.marketplace == "Amazon"
|
assert result.marketplace == "Amazon"
|
||||||
# Check the correct field based on your model
|
assert result.shop_id == test_shop.id
|
||||||
assert result.shop_id == test_shop.id # Changed from shop_code to shop_id
|
assert result.user_id == test_user.id
|
||||||
assert result.user_id == test_user.id if hasattr(result, "user_id") else True
|
|
||||||
assert result.status == "pending"
|
assert result.status == "pending"
|
||||||
assert result.source_url == "https://example.com/products.csv"
|
assert result.source_url == "https://example.com/products.csv"
|
||||||
|
assert result.shop_name == test_shop.shop_name
|
||||||
|
|
||||||
def test_create_import_job_invalid_shop(self, db, test_user):
|
def test_create_import_job_invalid_shop(self, db, test_user):
|
||||||
"""Test import job creation with invalid shop"""
|
"""Test import job creation with invalid shop"""
|
||||||
@@ -83,9 +101,32 @@ class TestMarketplaceService:
|
|||||||
batch_size=1000,
|
batch_size=1000,
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="Shop not found"):
|
with pytest.raises(ShopNotFoundException) as exc_info:
|
||||||
self.service.create_import_job(db, request, test_user)
|
self.service.create_import_job(db, request, test_user)
|
||||||
|
|
||||||
|
exception = exc_info.value
|
||||||
|
assert exception.error_code == "SHOP_NOT_FOUND"
|
||||||
|
assert "INVALID_SHOP" in exception.message
|
||||||
|
|
||||||
|
def test_create_import_job_unauthorized_access(self, db, test_shop, test_user, other_user):
|
||||||
|
"""Test import job creation with unauthorized shop access"""
|
||||||
|
# Set the shop owner to a different user
|
||||||
|
test_shop.owner_id = other_user.id
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
request = MarketplaceImportRequest(
|
||||||
|
url="https://example.com/products.csv",
|
||||||
|
marketplace="Amazon",
|
||||||
|
shop_code=test_shop.shop_code,
|
||||||
|
batch_size=1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(UnauthorizedShopAccessException) as exc_info:
|
||||||
|
self.service.create_import_job(db, request, test_user)
|
||||||
|
|
||||||
|
exception = exc_info.value
|
||||||
|
assert exception.error_code == "UNAUTHORIZED_SHOP_ACCESS"
|
||||||
|
|
||||||
def test_get_import_job_by_id_success(self, db, test_marketplace_job, test_user):
|
def test_get_import_job_by_id_success(self, db, test_marketplace_job, test_user):
|
||||||
"""Test getting import job by ID for job owner"""
|
"""Test getting import job by ID for job owner"""
|
||||||
result = self.service.get_import_job_by_id(
|
result = self.service.get_import_job_by_id(
|
||||||
@@ -93,9 +134,7 @@ class TestMarketplaceService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result.id == test_marketplace_job.id
|
assert result.id == test_marketplace_job.id
|
||||||
# Check user_id if the field exists
|
assert result.user_id == test_user.id
|
||||||
if hasattr(result, "user_id"):
|
|
||||||
assert result.user_id == test_user.id
|
|
||||||
|
|
||||||
def test_get_import_job_by_id_admin_access(
|
def test_get_import_job_by_id_admin_access(
|
||||||
self, db, test_marketplace_job, test_admin
|
self, db, test_marketplace_job, test_admin
|
||||||
@@ -109,25 +148,33 @@ class TestMarketplaceService:
|
|||||||
|
|
||||||
def test_get_import_job_by_id_not_found(self, db, test_user):
|
def test_get_import_job_by_id_not_found(self, db, test_user):
|
||||||
"""Test getting non-existent import job"""
|
"""Test getting non-existent import job"""
|
||||||
with pytest.raises(ValueError, match="Marketplace import job not found"):
|
with pytest.raises(ImportJobNotFoundException) as exc_info:
|
||||||
self.service.get_import_job_by_id(db, 99999, test_user)
|
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(
|
def test_get_import_job_by_id_access_denied(
|
||||||
self, db, test_marketplace_job, other_user
|
self, db, test_marketplace_job, other_user
|
||||||
):
|
):
|
||||||
"""Test access denied when user doesn't own the job"""
|
"""Test access denied when user doesn't own the job"""
|
||||||
with pytest.raises(PermissionError, match="Access denied to this import job"):
|
with pytest.raises(ImportJobNotOwnedException) as exc_info:
|
||||||
self.service.get_import_job_by_id(db, test_marketplace_job.id, other_user)
|
self.service.get_import_job_by_id(db, test_marketplace_job.id, other_user)
|
||||||
|
|
||||||
|
exception = exc_info.value
|
||||||
|
assert exception.error_code == "IMPORT_JOB_NOT_OWNED"
|
||||||
|
assert exception.status_code == 403
|
||||||
|
assert str(test_marketplace_job.id) in exception.message
|
||||||
|
|
||||||
def test_get_import_jobs_user_filter(self, db, test_marketplace_job, test_user):
|
def test_get_import_jobs_user_filter(self, db, test_marketplace_job, test_user):
|
||||||
"""Test getting import jobs filtered by user"""
|
"""Test getting import jobs filtered by user"""
|
||||||
jobs = self.service.get_import_jobs(db, test_user)
|
jobs = self.service.get_import_jobs(db, test_user)
|
||||||
|
|
||||||
assert len(jobs) >= 1
|
assert len(jobs) >= 1
|
||||||
assert any(job.id == test_marketplace_job.id for job in jobs)
|
assert any(job.id == test_marketplace_job.id for job in jobs)
|
||||||
# Check user_id if the field exists
|
assert test_marketplace_job.user_id == test_user.id
|
||||||
if hasattr(test_marketplace_job, "user_id"):
|
|
||||||
assert test_marketplace_job.user_id == test_user.id
|
|
||||||
|
|
||||||
def test_get_import_jobs_admin_sees_all(self, db, test_marketplace_job, test_admin):
|
def test_get_import_jobs_admin_sees_all(self, db, test_marketplace_job, test_admin):
|
||||||
"""Test that admin sees all import jobs"""
|
"""Test that admin sees all import jobs"""
|
||||||
@@ -158,7 +205,7 @@ class TestMarketplaceService:
|
|||||||
marketplace=f"Marketplace_{unique_id}_{i}",
|
marketplace=f"Marketplace_{unique_id}_{i}",
|
||||||
shop_name=f"Test_Shop_{unique_id}_{i}",
|
shop_name=f"Test_Shop_{unique_id}_{i}",
|
||||||
user_id=test_user.id,
|
user_id=test_user.id,
|
||||||
shop_id=test_shop.id, # Use shop_id instead of shop_code
|
shop_id=test_shop.id,
|
||||||
source_url=f"https://test-{i}.example.com/import",
|
source_url=f"https://test-{i}.example.com/import",
|
||||||
imported_count=0,
|
imported_count=0,
|
||||||
updated_count=0,
|
updated_count=0,
|
||||||
@@ -172,6 +219,15 @@ class TestMarketplaceService:
|
|||||||
|
|
||||||
assert len(jobs) <= 2 # Should be at most 2
|
assert len(jobs) <= 2 # Should be at most 2
|
||||||
|
|
||||||
|
def test_get_import_jobs_database_error(self, db_with_error, test_user):
|
||||||
|
"""Test getting import jobs handles database errors"""
|
||||||
|
with pytest.raises(ValidationException) as exc_info:
|
||||||
|
self.service.get_import_jobs(db_with_error, test_user)
|
||||||
|
|
||||||
|
exception = exc_info.value
|
||||||
|
assert exception.error_code == "VALIDATION_ERROR"
|
||||||
|
assert "Failed to retrieve import jobs" in exception.message
|
||||||
|
|
||||||
def test_update_job_status_success(self, db, test_marketplace_job):
|
def test_update_job_status_success(self, db, test_marketplace_job):
|
||||||
"""Test updating job status"""
|
"""Test updating job status"""
|
||||||
result = self.service.update_job_status(
|
result = self.service.update_job_status(
|
||||||
@@ -188,9 +244,22 @@ class TestMarketplaceService:
|
|||||||
|
|
||||||
def test_update_job_status_not_found(self, db):
|
def test_update_job_status_not_found(self, db):
|
||||||
"""Test updating non-existent job status"""
|
"""Test updating non-existent job status"""
|
||||||
with pytest.raises(ValueError, match="Marketplace import job not found"):
|
with pytest.raises(ImportJobNotFoundException) as exc_info:
|
||||||
self.service.update_job_status(db, 99999, "completed")
|
self.service.update_job_status(db, 99999, "completed")
|
||||||
|
|
||||||
|
exception = exc_info.value
|
||||||
|
assert exception.error_code == "IMPORT_JOB_NOT_FOUND"
|
||||||
|
assert "99999" in exception.message
|
||||||
|
|
||||||
|
def test_update_job_status_database_error(self, db_with_error):
|
||||||
|
"""Test updating job status handles database errors"""
|
||||||
|
with pytest.raises(ValidationException) as exc_info:
|
||||||
|
self.service.update_job_status(db_with_error, 1, "completed")
|
||||||
|
|
||||||
|
exception = exc_info.value
|
||||||
|
assert exception.error_code == "VALIDATION_ERROR"
|
||||||
|
assert "Failed to update job status" in exception.message
|
||||||
|
|
||||||
def test_get_job_stats_user(self, db, test_marketplace_job, test_user):
|
def test_get_job_stats_user(self, db, test_marketplace_job, test_user):
|
||||||
"""Test getting job statistics for user"""
|
"""Test getting job statistics for user"""
|
||||||
stats = self.service.get_job_stats(db, test_user)
|
stats = self.service.get_job_stats(db, test_user)
|
||||||
@@ -200,12 +269,23 @@ class TestMarketplaceService:
|
|||||||
assert "running_jobs" in stats
|
assert "running_jobs" in stats
|
||||||
assert "completed_jobs" in stats
|
assert "completed_jobs" in stats
|
||||||
assert "failed_jobs" in stats
|
assert "failed_jobs" in stats
|
||||||
|
assert isinstance(stats["total_jobs"], int)
|
||||||
|
|
||||||
def test_get_job_stats_admin(self, db, test_marketplace_job, test_admin):
|
def test_get_job_stats_admin(self, db, test_marketplace_job, test_admin):
|
||||||
"""Test getting job statistics for admin"""
|
"""Test getting job statistics for admin"""
|
||||||
stats = self.service.get_job_stats(db, test_admin)
|
stats = self.service.get_job_stats(db, test_admin)
|
||||||
|
|
||||||
assert stats["total_jobs"] >= 1
|
assert stats["total_jobs"] >= 1
|
||||||
|
assert isinstance(stats["total_jobs"], int)
|
||||||
|
|
||||||
|
def test_get_job_stats_database_error(self, db_with_error, test_user):
|
||||||
|
"""Test getting job stats handles database errors"""
|
||||||
|
with pytest.raises(ValidationException) as exc_info:
|
||||||
|
self.service.get_job_stats(db_with_error, test_user)
|
||||||
|
|
||||||
|
exception = exc_info.value
|
||||||
|
assert exception.error_code == "VALIDATION_ERROR"
|
||||||
|
assert "Failed to retrieve job statistics" in exception.message
|
||||||
|
|
||||||
def test_convert_to_response_model(self, test_marketplace_job):
|
def test_convert_to_response_model(self, test_marketplace_job):
|
||||||
"""Test converting database model to response model"""
|
"""Test converting database model to response model"""
|
||||||
@@ -226,7 +306,7 @@ class TestMarketplaceService:
|
|||||||
marketplace="Amazon",
|
marketplace="Amazon",
|
||||||
shop_name=f"TEST_SHOP_{unique_id}",
|
shop_name=f"TEST_SHOP_{unique_id}",
|
||||||
user_id=test_user.id,
|
user_id=test_user.id,
|
||||||
shop_id=test_shop.id, # Use shop_id instead of shop_code
|
shop_id=test_shop.id,
|
||||||
source_url="https://test.example.com/import",
|
source_url="https://test.example.com/import",
|
||||||
imported_count=0,
|
imported_count=0,
|
||||||
updated_count=0,
|
updated_count=0,
|
||||||
@@ -242,6 +322,22 @@ class TestMarketplaceService:
|
|||||||
assert result.status == "cancelled"
|
assert result.status == "cancelled"
|
||||||
assert result.completed_at is not None
|
assert result.completed_at is not None
|
||||||
|
|
||||||
|
def test_cancel_import_job_not_found(self, db, test_user):
|
||||||
|
"""Test cancelling non-existent import job"""
|
||||||
|
with pytest.raises(ImportJobNotFoundException) as exc_info:
|
||||||
|
self.service.cancel_import_job(db, 99999, test_user)
|
||||||
|
|
||||||
|
exception = exc_info.value
|
||||||
|
assert exception.error_code == "IMPORT_JOB_NOT_FOUND"
|
||||||
|
|
||||||
|
def test_cancel_import_job_access_denied(self, db, test_marketplace_job, other_user):
|
||||||
|
"""Test cancelling import job without access"""
|
||||||
|
with pytest.raises(ImportJobNotOwnedException) as exc_info:
|
||||||
|
self.service.cancel_import_job(db, test_marketplace_job.id, other_user)
|
||||||
|
|
||||||
|
exception = exc_info.value
|
||||||
|
assert exception.error_code == "IMPORT_JOB_NOT_OWNED"
|
||||||
|
|
||||||
def test_cancel_import_job_invalid_status(
|
def test_cancel_import_job_invalid_status(
|
||||||
self, db, test_marketplace_job, test_user
|
self, db, test_marketplace_job, test_user
|
||||||
):
|
):
|
||||||
@@ -250,11 +346,14 @@ class TestMarketplaceService:
|
|||||||
test_marketplace_job.status = "completed"
|
test_marketplace_job.status = "completed"
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(ImportJobCannotBeCancelledException) as exc_info:
|
||||||
ValueError, match="Cannot cancel job with status: completed"
|
|
||||||
):
|
|
||||||
self.service.cancel_import_job(db, test_marketplace_job.id, test_user)
|
self.service.cancel_import_job(db, test_marketplace_job.id, test_user)
|
||||||
|
|
||||||
|
exception = exc_info.value
|
||||||
|
assert exception.error_code == "IMPORT_JOB_CANNOT_BE_CANCELLED"
|
||||||
|
assert exception.status_code == 400
|
||||||
|
assert "completed" in exception.message
|
||||||
|
|
||||||
def test_delete_import_job_success(self, db, test_user, test_shop):
|
def test_delete_import_job_success(self, db, test_user, test_shop):
|
||||||
"""Test deleting a completed import job"""
|
"""Test deleting a completed import job"""
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
@@ -265,7 +364,7 @@ class TestMarketplaceService:
|
|||||||
marketplace="Amazon",
|
marketplace="Amazon",
|
||||||
shop_name=f"TEST_SHOP_{unique_id}",
|
shop_name=f"TEST_SHOP_{unique_id}",
|
||||||
user_id=test_user.id,
|
user_id=test_user.id,
|
||||||
shop_id=test_shop.id, # Use shop_id instead of shop_code
|
shop_id=test_shop.id,
|
||||||
source_url="https://test.example.com/import",
|
source_url="https://test.example.com/import",
|
||||||
imported_count=0,
|
imported_count=0,
|
||||||
updated_count=0,
|
updated_count=0,
|
||||||
@@ -289,6 +388,22 @@ class TestMarketplaceService:
|
|||||||
)
|
)
|
||||||
assert deleted_job is None
|
assert deleted_job is None
|
||||||
|
|
||||||
|
def test_delete_import_job_not_found(self, db, test_user):
|
||||||
|
"""Test deleting non-existent import job"""
|
||||||
|
with pytest.raises(ImportJobNotFoundException) as exc_info:
|
||||||
|
self.service.delete_import_job(db, 99999, test_user)
|
||||||
|
|
||||||
|
exception = exc_info.value
|
||||||
|
assert exception.error_code == "IMPORT_JOB_NOT_FOUND"
|
||||||
|
|
||||||
|
def test_delete_import_job_access_denied(self, db, test_marketplace_job, other_user):
|
||||||
|
"""Test deleting import job without access"""
|
||||||
|
with pytest.raises(ImportJobNotOwnedException) as exc_info:
|
||||||
|
self.service.delete_import_job(db, test_marketplace_job.id, other_user)
|
||||||
|
|
||||||
|
exception = exc_info.value
|
||||||
|
assert exception.error_code == "IMPORT_JOB_NOT_OWNED"
|
||||||
|
|
||||||
def test_delete_import_job_invalid_status(self, db, test_user, test_shop):
|
def test_delete_import_job_invalid_status(self, db, test_user, test_shop):
|
||||||
"""Test deleting a job that can't be deleted"""
|
"""Test deleting a job that can't be deleted"""
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
@@ -299,7 +414,7 @@ class TestMarketplaceService:
|
|||||||
marketplace="Amazon",
|
marketplace="Amazon",
|
||||||
shop_name=f"TEST_SHOP_{unique_id}",
|
shop_name=f"TEST_SHOP_{unique_id}",
|
||||||
user_id=test_user.id,
|
user_id=test_user.id,
|
||||||
shop_id=test_shop.id, # Use shop_id instead of shop_code
|
shop_id=test_shop.id,
|
||||||
source_url="https://test.example.com/import",
|
source_url="https://test.example.com/import",
|
||||||
imported_count=0,
|
imported_count=0,
|
||||||
updated_count=0,
|
updated_count=0,
|
||||||
@@ -310,5 +425,39 @@ class TestMarketplaceService:
|
|||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(job)
|
db.refresh(job)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="Cannot delete job with status: pending"):
|
with pytest.raises(ImportJobCannotBeDeletedException) as exc_info:
|
||||||
self.service.delete_import_job(db, job.id, test_user)
|
self.service.delete_import_job(db, job.id, test_user)
|
||||||
|
|
||||||
|
exception = exc_info.value
|
||||||
|
assert exception.error_code == "IMPORT_JOB_CANNOT_BE_DELETED"
|
||||||
|
assert exception.status_code == 400
|
||||||
|
assert "pending" in exception.message
|
||||||
|
|
||||||
|
# Test edge cases and error scenarios
|
||||||
|
def test_validate_shop_access_case_insensitive(self, db, test_shop, test_user):
|
||||||
|
"""Test shop access validation is case insensitive"""
|
||||||
|
test_shop.owner_id = test_user.id
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Test with lowercase shop code
|
||||||
|
result = self.service.validate_shop_access(db, test_shop.shop_code.lower(), test_user)
|
||||||
|
assert result.shop_code == test_shop.shop_code
|
||||||
|
|
||||||
|
# Test with uppercase shop code
|
||||||
|
result = self.service.validate_shop_access(db, test_shop.shop_code.upper(), test_user)
|
||||||
|
assert result.shop_code == test_shop.shop_code
|
||||||
|
|
||||||
|
def test_create_import_job_database_error(self, db_with_error, test_user):
|
||||||
|
"""Test import job creation handles database errors"""
|
||||||
|
request = MarketplaceImportRequest(
|
||||||
|
url="https://example.com/products.csv",
|
||||||
|
marketplace="Amazon",
|
||||||
|
shop_code="TEST_SHOP",
|
||||||
|
batch_size=1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValidationException) as exc_info:
|
||||||
|
self.service.create_import_job(db_with_error, request, test_user)
|
||||||
|
|
||||||
|
exception = exc_info.value
|
||||||
|
assert exception.error_code == "VALIDATION_ERROR"
|
||||||
|
|||||||
Reference in New Issue
Block a user