workflows testing
This commit is contained in:
73
tests/integration/api/v1/test_export.py
Normal file
73
tests/integration/api/v1/test_export.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# tests/test_export.py
|
||||
import csv
|
||||
from io import StringIO
|
||||
|
||||
import pytest
|
||||
|
||||
from models.database.product import Product
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.api
|
||||
@pytest.mark.performance # for the performance test
|
||||
class TestExportFunctionality:
|
||||
def test_csv_export_basic(self, client, auth_headers, test_product):
|
||||
"""Test basic CSV export functionality"""
|
||||
response = client.get("/api/v1/export-csv", headers=auth_headers)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"] == "text/csv; charset=utf-8"
|
||||
|
||||
# Parse CSV content
|
||||
csv_content = response.content.decode("utf-8")
|
||||
csv_reader = csv.reader(StringIO(csv_content))
|
||||
|
||||
# Check header row
|
||||
header = next(csv_reader)
|
||||
expected_fields = ["product_id", "title", "description", "price", "marketplace"]
|
||||
for field in expected_fields:
|
||||
assert field in header
|
||||
|
||||
def test_csv_export_with_marketplace_filter(self, client, auth_headers, db):
|
||||
"""Test CSV export with marketplace filtering"""
|
||||
# Create products in different marketplaces
|
||||
products = [
|
||||
Product(product_id="EXP1", title="Product 1", marketplace="Amazon"),
|
||||
Product(product_id="EXP2", title="Product 2", marketplace="eBay"),
|
||||
]
|
||||
|
||||
db.add_all(products)
|
||||
db.commit()
|
||||
|
||||
response = client.get(
|
||||
"/api/v1/export-csv?marketplace=Amazon", headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
csv_content = response.content.decode("utf-8")
|
||||
assert "EXP1" in csv_content
|
||||
assert "EXP2" not in csv_content # Should be filtered out
|
||||
|
||||
def test_csv_export_performance(self, client, auth_headers, db):
|
||||
"""Test CSV export performance with many products"""
|
||||
# Create many products
|
||||
products = []
|
||||
for i in range(1000):
|
||||
product = Product(
|
||||
product_id=f"PERF{i:04d}",
|
||||
title=f"Performance Product {i}",
|
||||
marketplace="Performance",
|
||||
)
|
||||
products.append(product)
|
||||
|
||||
db.add_all(products)
|
||||
db.commit()
|
||||
|
||||
import time
|
||||
|
||||
start_time = time.time()
|
||||
response = client.get("/api/v1/export-csv", headers=auth_headers)
|
||||
end_time = time.time()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert end_time - start_time < 10.0 # Should complete within 10 seconds
|
||||
116
tests/integration/api/v1/test_filtering.py
Normal file
116
tests/integration/api/v1/test_filtering.py
Normal file
@@ -0,0 +1,116 @@
|
||||
# tests/test_filtering.py
|
||||
import pytest
|
||||
|
||||
from models.database.product import Product
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.api
|
||||
@pytest.mark.products
|
||||
class TestFiltering:
|
||||
def test_product_brand_filter(self, client, auth_headers, db):
|
||||
"""Test filtering products by brand"""
|
||||
# Create products with different brands
|
||||
products = [
|
||||
Product(product_id="BRAND1", title="Product 1", brand="BrandA"),
|
||||
Product(product_id="BRAND2", title="Product 2", brand="BrandB"),
|
||||
Product(product_id="BRAND3", title="Product 3", brand="BrandA"),
|
||||
]
|
||||
|
||||
db.add_all(products)
|
||||
db.commit()
|
||||
|
||||
# Filter by BrandA
|
||||
response = client.get("/api/v1/product?brand=BrandA", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 2
|
||||
|
||||
# Filter by BrandB
|
||||
response = client.get("/api/v1/product?brand=BrandB", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 1
|
||||
|
||||
def test_product_marketplace_filter(self, client, auth_headers, db):
|
||||
"""Test filtering products by marketplace"""
|
||||
products = [
|
||||
Product(product_id="MKT1", title="Product 1", marketplace="Amazon"),
|
||||
Product(product_id="MKT2", title="Product 2", marketplace="eBay"),
|
||||
Product(product_id="MKT3", title="Product 3", marketplace="Amazon"),
|
||||
]
|
||||
|
||||
db.add_all(products)
|
||||
db.commit()
|
||||
|
||||
response = client.get(
|
||||
"/api/v1/product?marketplace=Amazon", headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 2
|
||||
|
||||
def test_product_search_filter(self, client, auth_headers, db):
|
||||
"""Test searching products by text"""
|
||||
products = [
|
||||
Product(
|
||||
product_id="SEARCH1", title="Apple iPhone", description="Smartphone"
|
||||
),
|
||||
Product(
|
||||
product_id="SEARCH2",
|
||||
title="Samsung Galaxy",
|
||||
description="Android phone",
|
||||
),
|
||||
Product(
|
||||
product_id="SEARCH3", title="iPad Tablet", description="Apple tablet"
|
||||
),
|
||||
]
|
||||
|
||||
db.add_all(products)
|
||||
db.commit()
|
||||
|
||||
# Search for "Apple"
|
||||
response = client.get("/api/v1/product?search=Apple", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 2 # iPhone and iPad
|
||||
|
||||
# Search for "phone"
|
||||
response = client.get("/api/v1/product?search=phone", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 2 # iPhone and Galaxy
|
||||
|
||||
def test_combined_filters(self, client, auth_headers, db):
|
||||
"""Test combining multiple filters"""
|
||||
products = [
|
||||
Product(
|
||||
product_id="COMBO1",
|
||||
title="Apple iPhone",
|
||||
brand="Apple",
|
||||
marketplace="Amazon",
|
||||
),
|
||||
Product(
|
||||
product_id="COMBO2",
|
||||
title="Apple iPad",
|
||||
brand="Apple",
|
||||
marketplace="eBay",
|
||||
),
|
||||
Product(
|
||||
product_id="COMBO3",
|
||||
title="Samsung Phone",
|
||||
brand="Samsung",
|
||||
marketplace="Amazon",
|
||||
),
|
||||
]
|
||||
|
||||
db.add_all(products)
|
||||
db.commit()
|
||||
|
||||
# Filter by brand AND marketplace
|
||||
response = client.get(
|
||||
"/api/v1/product?brand=Apple&marketplace=Amazon", headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 1 # Only iPhone matches both
|
||||
203
tests/integration/tasks/test_background_tasks.py
Normal file
203
tests/integration/tasks/test_background_tasks.py
Normal file
@@ -0,0 +1,203 @@
|
||||
# tests/test_background_tasks.py
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.tasks.background_tasks import process_marketplace_import
|
||||
from models.database.marketplace 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_shop):
|
||||
"""Test successful marketplace import background task"""
|
||||
# Create import job
|
||||
job = MarketplaceImportJob(
|
||||
status="pending",
|
||||
source_url="http://example.com/test.csv",
|
||||
shop_name="TESTSHOP",
|
||||
marketplace="TestMarket",
|
||||
shop_id=test_shop.id,
|
||||
user_id=test_user.id,
|
||||
)
|
||||
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", "TESTSHOP", 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_shop):
|
||||
"""Test marketplace import failure handling"""
|
||||
# Create import job
|
||||
job = MarketplaceImportJob(
|
||||
status="pending",
|
||||
source_url="http://example.com/test.csv",
|
||||
shop_name="TESTSHOP",
|
||||
marketplace="TestMarket",
|
||||
shop_id=test_shop.id,
|
||||
user_id=test_user.id,
|
||||
)
|
||||
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",
|
||||
"TESTSHOP",
|
||||
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",
|
||||
"TESTSHOP",
|
||||
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_shop):
|
||||
"""Test marketplace import with some errors"""
|
||||
# Create import job
|
||||
job = MarketplaceImportJob(
|
||||
status="pending",
|
||||
source_url="http://example.com/test.csv",
|
||||
shop_name="TESTSHOP",
|
||||
marketplace="TestMarket",
|
||||
shop_id=test_shop.id,
|
||||
user_id=test_user.id,
|
||||
)
|
||||
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", "TESTSHOP", 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
|
||||
130
tests/integration/workflows/test_integration.py
Normal file
130
tests/integration/workflows/test_integration.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# tests/test_integration.py
|
||||
import pytest
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.api
|
||||
@pytest.mark.e2e
|
||||
class TestIntegrationFlows:
|
||||
def test_full_product_workflow(self, client, auth_headers):
|
||||
"""Test complete product creation and management workflow"""
|
||||
# 1. Create a product
|
||||
product_data = {
|
||||
"product_id": "FLOW001",
|
||||
"title": "Integration Test Product",
|
||||
"description": "Testing full workflow",
|
||||
"price": "29.99",
|
||||
"brand": "FlowBrand",
|
||||
"gtin": "1111222233334",
|
||||
"availability": "in stock",
|
||||
"marketplace": "TestFlow",
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/product", headers=auth_headers, json=product_data
|
||||
)
|
||||
assert response.status_code == 200
|
||||
product = response.json()
|
||||
|
||||
# 2. Add stock for the product
|
||||
stock_data = {
|
||||
"gtin": product["gtin"],
|
||||
"location": "MAIN_WAREHOUSE",
|
||||
"quantity": 50,
|
||||
}
|
||||
|
||||
response = client.post("/api/v1/stock", headers=auth_headers, json=stock_data)
|
||||
assert response.status_code == 200
|
||||
|
||||
# 3. Get product with stock info
|
||||
response = client.get(
|
||||
f"/api/v1/product/{product['product_id']}", headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
product_detail = response.json()
|
||||
assert product_detail["stock_info"]["total_quantity"] == 50
|
||||
|
||||
# 4. Update product
|
||||
update_data = {"title": "Updated Integration Test Product"}
|
||||
response = client.put(
|
||||
f"/api/v1/product/{product['product_id']}",
|
||||
headers=auth_headers,
|
||||
json=update_data,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# 5. Search for product
|
||||
response = client.get(
|
||||
"/api/v1/product?search=Updated Integration", headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["total"] == 1
|
||||
|
||||
def test_shop_product_workflow(self, client, auth_headers):
|
||||
"""Test shop creation and product management workflow"""
|
||||
# 1. Create a shop
|
||||
shop_data = {
|
||||
"shop_code": "FLOWSHOP",
|
||||
"shop_name": "Integration Flow Shop",
|
||||
"description": "Test shop for integration",
|
||||
}
|
||||
|
||||
response = client.post("/api/v1/shop", headers=auth_headers, json=shop_data)
|
||||
assert response.status_code == 200
|
||||
shop = response.json()
|
||||
|
||||
# 2. Create a product
|
||||
product_data = {
|
||||
"product_id": "SHOPFLOW001",
|
||||
"title": "Shop Flow Product",
|
||||
"price": "15.99",
|
||||
"marketplace": "ShopFlow",
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/product", headers=auth_headers, json=product_data
|
||||
)
|
||||
assert response.status_code == 200
|
||||
product = response.json()
|
||||
|
||||
# 3. Add product to shop (if endpoint exists)
|
||||
# This would test the shop-product association
|
||||
|
||||
# 4. Get shop details
|
||||
response = client.get(f"/api/v1/shop/{shop['shop_code']}", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_stock_operations_workflow(self, client, auth_headers):
|
||||
"""Test complete stock management workflow"""
|
||||
gtin = "9999888877776"
|
||||
location = "TEST_WAREHOUSE"
|
||||
|
||||
# 1. Set initial stock
|
||||
response = client.post(
|
||||
"/api/v1/stock",
|
||||
headers=auth_headers,
|
||||
json={"gtin": gtin, "location": location, "quantity": 100},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# 2. Add more stock
|
||||
response = client.post(
|
||||
"/api/v1/stock/add",
|
||||
headers=auth_headers,
|
||||
json={"gtin": gtin, "location": location, "quantity": 25},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["quantity"] == 125
|
||||
|
||||
# 3. Remove some stock
|
||||
response = client.post(
|
||||
"/api/v1/stock/remove",
|
||||
headers=auth_headers,
|
||||
json={"gtin": gtin, "location": location, "quantity": 30},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["quantity"] == 95
|
||||
|
||||
# 4. Check total stock
|
||||
response = client.get(f"/api/v1/stock/{gtin}/total", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["total_quantity"] == 95
|
||||
72
tests/unit/middleware/test_middleware.py
Normal file
72
tests/unit/middleware/test_middleware.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# tests/test_middleware.py
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from middleware.auth import AuthManager
|
||||
from middleware.rate_limiter import RateLimiter
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.auth # for auth manager tests
|
||||
class TestRateLimiter:
|
||||
def test_rate_limiter_allows_requests(self):
|
||||
"""Test rate limiter allows requests within limit"""
|
||||
limiter = RateLimiter()
|
||||
client_id = "test_client"
|
||||
|
||||
# Should allow first request
|
||||
assert (
|
||||
limiter.allow_request(client_id, max_requests=10, window_seconds=3600)
|
||||
is True
|
||||
)
|
||||
|
||||
# Should allow subsequent requests within limit
|
||||
for _ in range(5):
|
||||
assert (
|
||||
limiter.allow_request(client_id, max_requests=10, window_seconds=3600)
|
||||
is True
|
||||
)
|
||||
|
||||
def test_rate_limiter_blocks_excess_requests(self):
|
||||
"""Test rate limiter blocks requests exceeding limit"""
|
||||
limiter = RateLimiter()
|
||||
client_id = "test_client_blocked"
|
||||
max_requests = 3
|
||||
|
||||
# Use up the allowed requests
|
||||
for _ in range(max_requests):
|
||||
assert limiter.allow_request(client_id, max_requests, 3600) is True
|
||||
|
||||
# Next request should be blocked
|
||||
assert limiter.allow_request(client_id, max_requests, 3600) is False
|
||||
|
||||
|
||||
class TestAuthManager:
|
||||
def test_password_hashing_and_verification(self):
|
||||
"""Test password hashing and verification"""
|
||||
auth_manager = AuthManager()
|
||||
password = "test_password_123"
|
||||
|
||||
# Hash password
|
||||
hashed = auth_manager.hash_password(password)
|
||||
|
||||
# Verify correct password
|
||||
assert auth_manager.verify_password(password, hashed) is True
|
||||
|
||||
# Verify incorrect password
|
||||
assert auth_manager.verify_password("wrong_password", hashed) is False
|
||||
|
||||
def test_jwt_token_creation_and_validation(self, test_user):
|
||||
"""Test JWT token creation and validation"""
|
||||
auth_manager = AuthManager()
|
||||
|
||||
# Create token
|
||||
token_data = auth_manager.create_access_token(test_user)
|
||||
|
||||
assert "access_token" in token_data
|
||||
assert token_data["token_type"] == "bearer"
|
||||
assert isinstance(token_data["expires_in"], int)
|
||||
|
||||
# Token should be a string
|
||||
assert isinstance(token_data["access_token"], str)
|
||||
assert len(token_data["access_token"]) > 50 # JWT tokens are long
|
||||
Reference in New Issue
Block a user