Skip to content

Commit 8cf628d

Browse files
updated unit tests
1 parent 5ff63a3 commit 8cf628d

File tree

10 files changed

+259
-305
lines changed

10 files changed

+259
-305
lines changed

content-gen/src/backend/requirements-dev.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ pytest>=8.0.0
77
pytest-asyncio>=0.23.0
88
pytest-cov>=5.0.0
99
pytest-mock>=3.14.0
10-
httpx>=0.27.0
11-
quart-cors>=0.7.0
1210

1311
# Code Quality
1412
black>=24.0.0
Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,25 @@ addopts =
1414
-v
1515
--strict-markers
1616
--tb=short
17-
--cov=../backend
17+
--cov=backend
1818
--cov-report=term-missing
1919
--cov-report=html:coverage_html
2020
--cov-report=xml:coverage.xml
2121
--cov-fail-under=20
22-
-p no:warnings
22+
23+
# Filter warnings
24+
filterwarnings =
25+
ignore::DeprecationWarning
26+
ignore::PendingDeprecationWarning
27+
ignore:Unclosed client session:ResourceWarning
28+
ignore:Unclosed connector:ResourceWarning
2329

2430
# Test paths
25-
testpaths = .
31+
testpaths = tests
2632

2733
# Coverage configuration
2834
[coverage:run]
29-
source = ../backend
35+
source = backend
3036
omit =
3137
tests/*
3238
*/tests/*
@@ -44,6 +50,6 @@ exclude_lines =
4450
def __repr__
4551
raise AssertionError
4652
raise NotImplementedError
47-
if __name__ == .__main__.:
53+
if __name__ == "__main__":
4854
if TYPE_CHECKING:
4955
@abstract

content-gen/src/tests/agents/test_image_content_agent.py

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -501,28 +501,67 @@ def test_truncate_with_eggshell_finish():
501501

502502
@pytest.mark.asyncio
503503
async def test_generate_image_truncates_very_long_prompt():
504-
"""Test generate_image handles very long prompts by truncating."""
504+
"""Test that _generate_dalle_image truncates very long product descriptions.
505+
506+
Verifies that when a very long product description is passed, it gets
507+
truncated before being sent to the OpenAI API.
508+
"""
505509
with patch("agents.image_content_agent.app_settings") as mock_settings, \
506-
patch("agents.image_content_agent._generate_dalle_image") as mock_dalle:
510+
patch("agents.image_content_agent.DefaultAzureCredential") as mock_cred, \
511+
patch("agents.image_content_agent.AsyncAzureOpenAI") as mock_client:
507512

513+
# Setup settings (using correct attribute names matching settings.py)
508514
mock_settings.azure_openai.effective_image_model = "dall-e-3"
509-
mock_settings.brand.primary_color = "#FF0000"
510-
mock_settings.brand.secondary_color = "#00FF00"
511-
mock_dalle.return_value = {"success": True, "image_base64": "abc123"}
515+
mock_settings.azure_openai.image_endpoint = "https://test.openai.azure.com"
516+
mock_settings.azure_openai.endpoint = "https://test.openai.azure.com"
517+
mock_settings.azure_openai.preview_api_version = "2024-02-15-preview"
518+
mock_settings.azure_openai.image_model = "dall-e-3"
519+
mock_settings.azure_openai.image_size = "1024x1024"
520+
mock_settings.azure_openai.image_quality = "standard"
521+
mock_settings.base_settings.azure_client_id = None
522+
mock_settings.brand_guidelines.get_image_generation_prompt.return_value = "Brand style"
523+
mock_settings.brand_guidelines.primary_color = "#FF0000"
524+
mock_settings.brand_guidelines.secondary_color = "#00FF00"
525+
526+
# Setup credential mock
527+
mock_credential = AsyncMock()
528+
mock_token = MagicMock()
529+
mock_token.token = "test-token"
530+
mock_credential.get_token = AsyncMock(return_value=mock_token)
531+
mock_cred.return_value = mock_credential
532+
533+
# Setup OpenAI client mock - capture the prompt argument
534+
mock_openai = AsyncMock()
535+
mock_image_data = MagicMock()
536+
mock_image_data.b64_json = base64.b64encode(b"fake-image").decode()
537+
mock_image_data.revised_prompt = None
538+
mock_response = MagicMock()
539+
mock_response.data = [mock_image_data]
540+
mock_openai.images.generate = AsyncMock(return_value=mock_response)
541+
mock_openai.close = AsyncMock()
542+
mock_client.return_value = mock_openai
512543

513544
from agents.image_content_agent import generate_image
514545

515-
very_long_product_desc = "Product description. " * 500 # ~10000 chars
546+
# Create very long product description (~10000 chars)
547+
very_long_product_desc = "Product description with details. " * 300
516548

517-
_ = await generate_image(
549+
result = await generate_image(
518550
prompt="Create marketing image",
519551
product_description=very_long_product_desc,
520552
scene_description="Modern kitchen"
521553
)
522554

523-
# Should still succeed despite long input
524-
mock_dalle.assert_called_once()
525-
# Verify prompt was truncated (check call args)
526-
call_args = mock_dalle.call_args
527-
actual_prompt = call_args[1]["prompt"] if call_args[1] else call_args[0][0]
528-
assert len(actual_prompt) <= 4200 # Should be truncated
555+
# Verify success
556+
assert result["success"] is True
557+
558+
# Verify the prompt was truncated before being sent to OpenAI
559+
call_kwargs = mock_openai.images.generate.call_args.kwargs
560+
prompt_sent = call_kwargs["prompt"]
561+
562+
# The full prompt should be under DALL-E's limit (~4000 chars)
563+
# despite the ~10000 char input
564+
assert len(prompt_sent) < 4000, f"Prompt not truncated: {len(prompt_sent)} chars"
565+
566+
# Also verify via prompt_used in result
567+
assert len(result["prompt_used"]) < 4000

content-gen/src/tests/api/test_admin.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,11 @@ async def test_create_search_index_missing_endpoint(client):
720720

721721
@pytest.mark.asyncio
722722
async def test_upload_images_validation_error(client):
723-
"""Test upload images endpoint validation."""
723+
"""Test upload images endpoint validation for missing data field.
724+
725+
The endpoint returns 200 with per-image results (not 400) for bulk operations,
726+
allowing partial success. Images missing required fields are marked as failed.
727+
"""
724728
# Missing required data field
725729
response = await client.post(
726730
"/api/admin/upload-images",
@@ -732,5 +736,16 @@ async def test_upload_images_validation_error(client):
732736
}
733737
)
734738

735-
# Should handle validation error
736-
assert response.status_code in [200, 400, 500]
739+
# Endpoint returns 200 with per-image results for bulk operations
740+
assert response.status_code == 200
741+
data = await response.get_json()
742+
743+
# Should indicate failure at the operation level
744+
assert data["success"] is False
745+
assert data["failed"] == 1
746+
assert data["uploaded"] == 0
747+
748+
# Should have detailed per-image failure info
749+
assert len(data["results"]) == 1
750+
assert data["results"][0]["status"] == "failed"
751+
assert "Missing filename or data" in data["results"][0]["error"]

content-gen/src/tests/conftest.py

Lines changed: 86 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"""
99

1010
import asyncio
11+
import gc
1112
import os
1213
import sys
1314
from datetime import datetime, timezone
@@ -16,68 +17,96 @@
1617
import pytest
1718
from quart import Quart
1819

19-
# Set environment variables BEFORE any backend imports
20-
# This prevents settings.py from failing during import
21-
os.environ.update({
22-
# Base settings
23-
"AZURE_OPENAI_ENDPOINT": "https://test-openai.openai.azure.com/",
24-
"AZURE_OPENAI_API_VERSION": "2024-08-01-preview",
25-
"AZURE_OPENAI_CHAT_DEPLOYMENT": "gpt-4o",
26-
"AZURE_OPENAI_EMBEDDING_DEPLOYMENT": "text-embedding-3-large",
27-
"AZURE_OPENAI_DALLE_DEPLOYMENT": "dall-e-3",
28-
"AZURE_CLIENT_ID": "test-client-id",
29-
30-
# Cosmos DB
31-
"AZURE_COSMOSDB_ENDPOINT": "https://test-cosmos.documents.azure.com:443/",
32-
"AZURE_COSMOSDB_DATABASE_NAME": "test-db",
33-
"AZURE_COSMOSDB_PRODUCTS_CONTAINER": "products",
34-
"AZURE_COSMOSDB_CONVERSATIONS_CONTAINER": "conversations",
35-
36-
# Blob Storage
37-
"AZURE_STORAGE_ACCOUNT_NAME": "teststorage",
38-
"AZURE_STORAGE_CONTAINER": "test-container",
39-
"AZURE_STORAGE_ACCOUNT_URL": "https://teststorage.blob.core.windows.net",
40-
"AZURE_BLOB_PRODUCT_IMAGES_CONTAINER": "product-images",
41-
"AZURE_BLOB_GENERATED_IMAGES_CONTAINER": "generated-images",
42-
43-
# Content Safety
44-
"AZURE_CONTENT_SAFETY_ENDPOINT": "https://test-safety.cognitiveservices.azure.com/",
45-
"AZURE_CONTENT_SAFETY_API_VERSION": "2024-09-01",
46-
47-
# Search Service
48-
"AZURE_SEARCH_ENDPOINT": "https://test-search.search.windows.net",
49-
"AZURE_SEARCH_INDEX_NAME": "products-index",
50-
51-
# Foundry (optional)
52-
"USE_FOUNDRY": "false",
53-
"AZURE_AI_PROJECT_CONNECTION_STRING": "",
54-
55-
# Admin - Empty for development mode (no authentication required)
56-
"ADMIN_API_KEY": "",
57-
58-
# App Configuration
59-
"ALLOWED_ORIGIN": "http://localhost:3000",
60-
"LOG_LEVEL": "DEBUG",
61-
})
62-
63-
# Add the backend directory to the Python path so we can import backend modules
64-
tests_dir = os.path.dirname(os.path.abspath(__file__))
65-
backend_dir = os.path.join(os.path.dirname(tests_dir), 'backend')
66-
if backend_dir not in sys.path:
67-
sys.path.insert(0, backend_dir)
68-
69-
# Set Windows event loop policy at module level (fixes pytest-asyncio auto mode compatibility)
70-
if sys.platform == "win32":
71-
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
20+
21+
def pytest_configure(config):
22+
"""Set minimal env vars required for backend imports before test collection.
23+
24+
Only sets variables absolutely required to import settings.py without errors.
25+
All other test environment configuration is handled by the mock_environment fixture.
26+
"""
27+
# AZURE_OPENAI_ENDPOINT is required by _AzureOpenAISettings validator
28+
os.environ.setdefault("AZURE_OPENAI_ENDPOINT", "https://test.openai.azure.com/")
29+
30+
# Add the backend directory to the Python path
31+
tests_dir = os.path.dirname(os.path.abspath(__file__))
32+
backend_dir = os.path.join(os.path.dirname(tests_dir), 'backend')
33+
if backend_dir not in sys.path:
34+
sys.path.insert(0, backend_dir)
35+
36+
# Set Windows event loop policy (fixes pytest-asyncio auto mode compatibility)
37+
if sys.platform == "win32":
38+
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
39+
40+
41+
def pytest_sessionfinish(session, exitstatus): # noqa: ARG001
42+
"""Clean up any remaining async resources after test session.
43+
44+
This helps prevent 'Unclosed client session' warnings from aiohttp
45+
that can occur when Azure SDK or other async clients aren't fully closed.
46+
47+
Args:
48+
session: pytest Session object (required by hook signature)
49+
exitstatus: exit status code (required by hook signature)
50+
"""
51+
del session, exitstatus # Unused but required by pytest hook signature
52+
# Force garbage collection to trigger cleanup of any unclosed sessions
53+
gc.collect()
54+
55+
# Close any remaining event loops
56+
try:
57+
loop = asyncio.get_event_loop()
58+
if loop.is_running():
59+
loop.stop()
60+
if not loop.is_closed():
61+
loop.close()
62+
except Exception:
63+
pass
7264

7365

7466
# ==================== Environment Configuration ====================
7567

7668
@pytest.fixture(scope="function", autouse=True)
77-
def mock_environment():
78-
"""Ensure environment variables are set for each test."""
79-
# Environment variables are already set at module level
80-
# This fixture exists for potential test-specific overrides
69+
def mock_environment(monkeypatch):
70+
"""Set test environment variables with correct names matching settings.py.
71+
72+
Uses monkeypatch for proper test isolation - each test starts with a clean
73+
environment and changes are automatically reverted after the test.
74+
"""
75+
env_vars = {
76+
# Azure OpenAI (required - _AzureOpenAISettings)
77+
"AZURE_OPENAI_ENDPOINT": "https://test-openai.openai.azure.com/",
78+
"AZURE_OPENAI_API_VERSION": "2024-08-01-preview",
79+
80+
# Azure Cosmos DB (_CosmosSettings uses AZURE_COSMOS_ prefix)
81+
"AZURE_COSMOS_ENDPOINT": "https://test-cosmos.documents.azure.com:443/",
82+
"AZURE_COSMOS_DATABASE_NAME": "test-db",
83+
84+
# Chat History (_ChatHistorySettings uses AZURE_COSMOSDB_ prefix)
85+
"AZURE_COSMOSDB_DATABASE": "test-db",
86+
"AZURE_COSMOSDB_ACCOUNT": "test-cosmos",
87+
"AZURE_COSMOSDB_CONVERSATIONS_CONTAINER": "conversations",
88+
"AZURE_COSMOSDB_PRODUCTS_CONTAINER": "products",
89+
90+
# Azure Blob Storage (_StorageSettings uses AZURE_BLOB_ prefix)
91+
"AZURE_BLOB_ACCOUNT_NAME": "teststorage",
92+
"AZURE_BLOB_PRODUCT_IMAGES_CONTAINER": "product-images",
93+
"AZURE_BLOB_GENERATED_IMAGES_CONTAINER": "generated-images",
94+
95+
# Azure AI Search (_SearchSettings uses AZURE_AI_SEARCH_ prefix)
96+
"AZURE_AI_SEARCH_ENDPOINT": "https://test-search.search.windows.net",
97+
"AZURE_AI_SEARCH_PRODUCTS_INDEX": "products",
98+
"AZURE_AI_SEARCH_IMAGE_INDEX": "product-images",
99+
100+
# AI Foundry (disabled for tests)
101+
"USE_FOUNDRY": "false",
102+
103+
# Admin API (empty = development mode, no auth required)
104+
"ADMIN_API_KEY": "",
105+
}
106+
107+
for key, value in env_vars.items():
108+
monkeypatch.setenv(key, value)
109+
81110
yield
82111

83112

0 commit comments

Comments
 (0)