Feature add test containers mail email code login task (#26580)

Signed-off-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
NeatGuyCoding 2025-10-05 12:44:40 +08:00 committed by GitHub
parent bbbb6e04cb
commit 00fb468f2e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 630 additions and 9 deletions

View File

@ -18,6 +18,7 @@ from flask.testing import FlaskClient
from sqlalchemy import Engine, text
from sqlalchemy.orm import Session
from testcontainers.core.container import DockerContainer
from testcontainers.core.network import Network
from testcontainers.core.waiting_utils import wait_for_logs
from testcontainers.postgres import PostgresContainer
from testcontainers.redis import RedisContainer
@ -41,6 +42,7 @@ class DifyTestContainers:
def __init__(self):
"""Initialize container management with default configurations."""
self.network: Network | None = None
self.postgres: PostgresContainer | None = None
self.redis: RedisContainer | None = None
self.dify_sandbox: DockerContainer | None = None
@ -62,12 +64,18 @@ class DifyTestContainers:
logger.info("Starting test containers for Dify integration tests...")
# Create Docker network for container communication
logger.info("Creating Docker network for container communication...")
self.network = Network()
self.network.create()
logger.info("Docker network created successfully with name: %s", self.network.name)
# Start PostgreSQL container for main application database
# PostgreSQL is used for storing user data, workflows, and application state
logger.info("Initializing PostgreSQL container...")
self.postgres = PostgresContainer(
image="postgres:14-alpine",
)
).with_network(self.network)
self.postgres.start()
db_host = self.postgres.get_container_host_ip()
db_port = self.postgres.get_exposed_port(5432)
@ -137,7 +145,7 @@ class DifyTestContainers:
# Start Redis container for caching and session management
# Redis is used for storing session data, cache entries, and temporary data
logger.info("Initializing Redis container...")
self.redis = RedisContainer(image="redis:6-alpine", port=6379)
self.redis = RedisContainer(image="redis:6-alpine", port=6379).with_network(self.network)
self.redis.start()
redis_host = self.redis.get_container_host_ip()
redis_port = self.redis.get_exposed_port(6379)
@ -153,7 +161,7 @@ class DifyTestContainers:
# Start Dify Sandbox container for code execution environment
# Dify Sandbox provides a secure environment for executing user code
logger.info("Initializing Dify Sandbox container...")
self.dify_sandbox = DockerContainer(image="langgenius/dify-sandbox:latest")
self.dify_sandbox = DockerContainer(image="langgenius/dify-sandbox:latest").with_network(self.network)
self.dify_sandbox.with_exposed_ports(8194)
self.dify_sandbox.env = {
"API_KEY": "test_api_key",
@ -173,22 +181,28 @@ class DifyTestContainers:
# Start Dify Plugin Daemon container for plugin management
# Dify Plugin Daemon provides plugin lifecycle management and execution
logger.info("Initializing Dify Plugin Daemon container...")
self.dify_plugin_daemon = DockerContainer(image="langgenius/dify-plugin-daemon:0.3.0-local")
self.dify_plugin_daemon = DockerContainer(image="langgenius/dify-plugin-daemon:0.3.0-local").with_network(
self.network
)
self.dify_plugin_daemon.with_exposed_ports(5002)
# Get container internal network addresses
postgres_container_name = self.postgres.get_wrapped_container().name
redis_container_name = self.redis.get_wrapped_container().name
self.dify_plugin_daemon.env = {
"DB_HOST": db_host,
"DB_PORT": str(db_port),
"DB_HOST": postgres_container_name, # Use container name for internal network communication
"DB_PORT": "5432", # Use internal port
"DB_USERNAME": self.postgres.username,
"DB_PASSWORD": self.postgres.password,
"DB_DATABASE": "dify_plugin",
"REDIS_HOST": redis_host,
"REDIS_PORT": str(redis_port),
"REDIS_HOST": redis_container_name, # Use container name for internal network communication
"REDIS_PORT": "6379", # Use internal port
"REDIS_PASSWORD": "",
"SERVER_PORT": "5002",
"SERVER_KEY": "test_plugin_daemon_key",
"MAX_PLUGIN_PACKAGE_SIZE": "52428800",
"PPROF_ENABLED": "false",
"DIFY_INNER_API_URL": f"http://{db_host}:5001",
"DIFY_INNER_API_URL": f"http://{postgres_container_name}:5001",
"DIFY_INNER_API_KEY": "test_inner_api_key",
"PLUGIN_REMOTE_INSTALLING_HOST": "0.0.0.0",
"PLUGIN_REMOTE_INSTALLING_PORT": "5003",
@ -253,6 +267,15 @@ class DifyTestContainers:
# Log error but don't fail the test cleanup
logger.warning("Failed to stop container %s: %s", container, e)
# Stop and remove the network
if self.network:
try:
logger.info("Removing Docker network...")
self.network.remove()
logger.info("Successfully removed Docker network")
except Exception as e:
logger.warning("Failed to remove Docker network: %s", e)
self._containers_started = False
logger.info("All test containers stopped and cleaned up successfully")

View File

@ -0,0 +1,598 @@
"""
TestContainers-based integration tests for send_email_code_login_mail_task.
This module provides comprehensive integration tests for the email code login mail task
using TestContainers infrastructure. The tests ensure that the task properly sends
email verification codes for login with internationalization support and handles
various error scenarios in a real database environment.
All tests use the testcontainers infrastructure to ensure proper database isolation
and realistic testing scenarios with actual PostgreSQL and Redis instances.
"""
from unittest.mock import MagicMock, patch
import pytest
from faker import Faker
from libs.email_i18n import EmailType
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
from tasks.mail_email_code_login import send_email_code_login_mail_task
class TestSendEmailCodeLoginMailTask:
"""
Comprehensive integration tests for send_email_code_login_mail_task using testcontainers.
This test class covers all major functionality of the email code login mail task:
- Successful email sending with different languages
- Email service integration and template rendering
- Error handling for various failure scenarios
- Performance metrics and logging verification
- Edge cases and boundary conditions
All tests use the testcontainers infrastructure to ensure proper database isolation
and realistic testing environment with actual database interactions.
"""
@pytest.fixture(autouse=True)
def cleanup_database(self, db_session_with_containers):
"""Clean up database before each test to ensure isolation."""
from extensions.ext_redis import redis_client
# Clear all test data
db_session_with_containers.query(TenantAccountJoin).delete()
db_session_with_containers.query(Tenant).delete()
db_session_with_containers.query(Account).delete()
db_session_with_containers.commit()
# Clear Redis cache
redis_client.flushdb()
@pytest.fixture
def mock_external_service_dependencies(self):
"""Mock setup for external service dependencies."""
with (
patch("tasks.mail_email_code_login.mail") as mock_mail,
patch("tasks.mail_email_code_login.get_email_i18n_service") as mock_email_service,
):
# Setup default mock returns
mock_mail.is_inited.return_value = True
# Mock email service
mock_email_service_instance = MagicMock()
mock_email_service_instance.send_email.return_value = None
mock_email_service.return_value = mock_email_service_instance
yield {
"mail": mock_mail,
"email_service": mock_email_service,
"email_service_instance": mock_email_service_instance,
}
def _create_test_account(self, db_session_with_containers, fake=None):
"""
Helper method to create a test account for testing.
Args:
db_session_with_containers: Database session from testcontainers infrastructure
fake: Faker instance for generating test data
Returns:
Account: Created account instance
"""
if fake is None:
fake = Faker()
# Create account
account = Account(
email=fake.email(),
name=fake.name(),
interface_language="en-US",
status="active",
)
db_session_with_containers.add(account)
db_session_with_containers.commit()
return account
def _create_test_tenant_and_account(self, db_session_with_containers, fake=None):
"""
Helper method to create a test tenant and account for testing.
Args:
db_session_with_containers: Database session from testcontainers infrastructure
fake: Faker instance for generating test data
Returns:
tuple: (Account, Tenant) created instances
"""
if fake is None:
fake = Faker()
# Create account using the existing helper method
account = self._create_test_account(db_session_with_containers, fake)
# Create tenant
tenant = Tenant(
name=fake.company(),
plan="basic",
status="active",
)
db_session_with_containers.add(tenant)
db_session_with_containers.commit()
# Create tenant-account relationship
tenant_account_join = TenantAccountJoin(
tenant_id=tenant.id,
account_id=account.id,
role=TenantAccountRole.OWNER,
)
db_session_with_containers.add(tenant_account_join)
db_session_with_containers.commit()
return account, tenant
def test_send_email_code_login_mail_task_success_english(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful email code login mail sending in English.
This test verifies that the task can successfully:
1. Send email code login mail with English language
2. Use proper email service integration
3. Pass correct template context to email service
4. Log performance metrics correctly
5. Complete task execution without errors
"""
# Arrange: Setup test data
fake = Faker()
test_email = fake.email()
test_code = "123456"
test_language = "en-US"
# Act: Execute the task
send_email_code_login_mail_task(
language=test_language,
to=test_email,
code=test_code,
)
# Assert: Verify expected outcomes
mock_mail = mock_external_service_dependencies["mail"]
mock_email_service_instance = mock_external_service_dependencies["email_service_instance"]
# Verify mail service was checked for initialization
mock_mail.is_inited.assert_called_once()
# Verify email service was called with correct parameters
mock_email_service_instance.send_email.assert_called_once_with(
email_type=EmailType.EMAIL_CODE_LOGIN,
language_code=test_language,
to=test_email,
template_context={
"to": test_email,
"code": test_code,
},
)
def test_send_email_code_login_mail_task_success_chinese(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful email code login mail sending in Chinese.
This test verifies that the task can successfully:
1. Send email code login mail with Chinese language
2. Handle different language codes properly
3. Use correct template context for Chinese emails
4. Complete task execution without errors
"""
# Arrange: Setup test data
fake = Faker()
test_email = fake.email()
test_code = "789012"
test_language = "zh-Hans"
# Act: Execute the task
send_email_code_login_mail_task(
language=test_language,
to=test_email,
code=test_code,
)
# Assert: Verify expected outcomes
mock_email_service_instance = mock_external_service_dependencies["email_service_instance"]
# Verify email service was called with Chinese language
mock_email_service_instance.send_email.assert_called_once_with(
email_type=EmailType.EMAIL_CODE_LOGIN,
language_code=test_language,
to=test_email,
template_context={
"to": test_email,
"code": test_code,
},
)
def test_send_email_code_login_mail_task_success_multiple_languages(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful email code login mail sending with multiple languages.
This test verifies that the task can successfully:
1. Handle various language codes correctly
2. Send emails with different language configurations
3. Maintain proper template context for each language
4. Complete multiple task executions without conflicts
"""
# Arrange: Setup test data
fake = Faker()
test_languages = ["en-US", "zh-Hans", "zh-CN", "ja-JP", "ko-KR"]
test_emails = [fake.email() for _ in test_languages]
test_codes = [fake.numerify("######") for _ in test_languages]
# Act: Execute the task for each language
for i, language in enumerate(test_languages):
send_email_code_login_mail_task(
language=language,
to=test_emails[i],
code=test_codes[i],
)
# Assert: Verify expected outcomes
mock_email_service_instance = mock_external_service_dependencies["email_service_instance"]
# Verify email service was called for each language
assert mock_email_service_instance.send_email.call_count == len(test_languages)
# Verify each call had correct parameters
for i, language in enumerate(test_languages):
call_args = mock_email_service_instance.send_email.call_args_list[i]
assert call_args[1]["email_type"] == EmailType.EMAIL_CODE_LOGIN
assert call_args[1]["language_code"] == language
assert call_args[1]["to"] == test_emails[i]
assert call_args[1]["template_context"]["code"] == test_codes[i]
def test_send_email_code_login_mail_task_mail_not_initialized(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test email code login mail task when mail service is not initialized.
This test verifies that the task can properly:
1. Check mail service initialization status
2. Return early when mail is not initialized
3. Not attempt to send email when service is unavailable
4. Handle gracefully without errors
"""
# Arrange: Setup test data
fake = Faker()
test_email = fake.email()
test_code = "123456"
test_language = "en-US"
# Mock mail service as not initialized
mock_mail = mock_external_service_dependencies["mail"]
mock_mail.is_inited.return_value = False
# Act: Execute the task
send_email_code_login_mail_task(
language=test_language,
to=test_email,
code=test_code,
)
# Assert: Verify expected outcomes
mock_email_service_instance = mock_external_service_dependencies["email_service_instance"]
# Verify mail service was checked for initialization
mock_mail.is_inited.assert_called_once()
# Verify email service was not called
mock_email_service_instance.send_email.assert_not_called()
def test_send_email_code_login_mail_task_email_service_exception(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test email code login mail task when email service raises an exception.
This test verifies that the task can properly:
1. Handle email service exceptions gracefully
2. Log appropriate error messages
3. Continue execution without crashing
4. Maintain proper error handling
"""
# Arrange: Setup test data
fake = Faker()
test_email = fake.email()
test_code = "123456"
test_language = "en-US"
# Mock email service to raise an exception
mock_email_service_instance = mock_external_service_dependencies["email_service_instance"]
mock_email_service_instance.send_email.side_effect = Exception("Email service unavailable")
# Act: Execute the task - it should handle the exception gracefully
send_email_code_login_mail_task(
language=test_language,
to=test_email,
code=test_code,
)
# Assert: Verify expected outcomes
mock_mail = mock_external_service_dependencies["mail"]
mock_email_service_instance = mock_external_service_dependencies["email_service_instance"]
# Verify mail service was checked for initialization
mock_mail.is_inited.assert_called_once()
# Verify email service was called (and failed)
mock_email_service_instance.send_email.assert_called_once_with(
email_type=EmailType.EMAIL_CODE_LOGIN,
language_code=test_language,
to=test_email,
template_context={
"to": test_email,
"code": test_code,
},
)
def test_send_email_code_login_mail_task_invalid_parameters(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test email code login mail task with invalid parameters.
This test verifies that the task can properly:
1. Handle empty or None email addresses
2. Process empty or None verification codes
3. Handle invalid language codes
4. Maintain proper error handling for invalid inputs
"""
# Arrange: Setup test data
fake = Faker()
test_language = "en-US"
# Test cases for invalid parameters
invalid_test_cases = [
{"email": "", "code": "123456", "description": "empty email"},
{"email": None, "code": "123456", "description": "None email"},
{"email": fake.email(), "code": "", "description": "empty code"},
{"email": fake.email(), "code": None, "description": "None code"},
{"email": "invalid-email", "code": "123456", "description": "invalid email format"},
]
for test_case in invalid_test_cases:
# Reset mocks for each test case
mock_email_service_instance = mock_external_service_dependencies["email_service_instance"]
mock_email_service_instance.reset_mock()
# Act: Execute the task with invalid parameters
send_email_code_login_mail_task(
language=test_language,
to=test_case["email"],
code=test_case["code"],
)
# Assert: Verify that email service was still called
# The task should pass parameters to email service as-is
# and let the email service handle validation
mock_email_service_instance.send_email.assert_called_once()
def test_send_email_code_login_mail_task_edge_cases(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test email code login mail task with edge cases and boundary conditions.
This test verifies that the task can properly:
1. Handle very long email addresses
2. Process very long verification codes
3. Handle special characters in parameters
4. Process extreme language codes
"""
# Arrange: Setup test data
fake = Faker()
test_language = "en-US"
# Edge case test data
edge_cases = [
{
"email": "a" * 100 + "@example.com", # Very long email
"code": "1" * 20, # Very long code
"description": "very long email and code",
},
{
"email": "test+tag@example.com", # Email with special characters
"code": "123-456", # Code with special characters
"description": "special characters",
},
{
"email": "test@sub.domain.example.com", # Complex domain
"code": "000000", # All zeros
"description": "complex domain and all zeros code",
},
{
"email": "test@example.co.uk", # International domain
"code": "999999", # All nines
"description": "international domain and all nines code",
},
]
for test_case in edge_cases:
# Reset mocks for each test case
mock_email_service_instance = mock_external_service_dependencies["email_service_instance"]
mock_email_service_instance.reset_mock()
# Act: Execute the task with edge case data
send_email_code_login_mail_task(
language=test_language,
to=test_case["email"],
code=test_case["code"],
)
# Assert: Verify that email service was called with edge case data
mock_email_service_instance.send_email.assert_called_once_with(
email_type=EmailType.EMAIL_CODE_LOGIN,
language_code=test_language,
to=test_case["email"],
template_context={
"to": test_case["email"],
"code": test_case["code"],
},
)
def test_send_email_code_login_mail_task_database_integration(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test email code login mail task with database integration.
This test verifies that the task can properly:
1. Work with real database connections
2. Handle database session management
3. Maintain proper database state
4. Complete without database-related errors
"""
# Arrange: Setup test data with database
fake = Faker()
account, tenant = self._create_test_tenant_and_account(db_session_with_containers, fake)
test_email = account.email
test_code = "123456"
test_language = "en-US"
# Act: Execute the task
send_email_code_login_mail_task(
language=test_language,
to=test_email,
code=test_code,
)
# Assert: Verify expected outcomes
mock_email_service_instance = mock_external_service_dependencies["email_service_instance"]
# Verify email service was called with database account email
mock_email_service_instance.send_email.assert_called_once_with(
email_type=EmailType.EMAIL_CODE_LOGIN,
language_code=test_language,
to=test_email,
template_context={
"to": test_email,
"code": test_code,
},
)
# Verify database state is maintained
db_session_with_containers.refresh(account)
assert account.email == test_email
assert account.status == "active"
def test_send_email_code_login_mail_task_redis_integration(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test email code login mail task with Redis integration.
This test verifies that the task can properly:
1. Work with Redis cache connections
2. Handle Redis operations without errors
3. Maintain proper cache state
4. Complete without Redis-related errors
"""
# Arrange: Setup test data
fake = Faker()
test_email = fake.email()
test_code = "123456"
test_language = "en-US"
# Setup Redis cache data
from extensions.ext_redis import redis_client
cache_key = f"email_code_login_test_{test_email}"
redis_client.set(cache_key, "test_value", ex=300)
# Act: Execute the task
send_email_code_login_mail_task(
language=test_language,
to=test_email,
code=test_code,
)
# Assert: Verify expected outcomes
mock_email_service_instance = mock_external_service_dependencies["email_service_instance"]
# Verify email service was called
mock_email_service_instance.send_email.assert_called_once()
# Verify Redis cache is still accessible
assert redis_client.exists(cache_key) == 1
assert redis_client.get(cache_key) == b"test_value"
# Clean up Redis cache
redis_client.delete(cache_key)
def test_send_email_code_login_mail_task_error_handling_comprehensive(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test comprehensive error handling for email code login mail task.
This test verifies that the task can properly:
1. Handle various types of exceptions
2. Log appropriate error messages
3. Continue execution despite errors
4. Maintain proper error reporting
"""
# Arrange: Setup test data
fake = Faker()
test_email = fake.email()
test_code = "123456"
test_language = "en-US"
# Test different exception types
exception_types = [
("ValueError", ValueError("Invalid email format")),
("RuntimeError", RuntimeError("Service unavailable")),
("ConnectionError", ConnectionError("Network error")),
("TimeoutError", TimeoutError("Request timeout")),
("Exception", Exception("Generic error")),
]
for error_name, exception in exception_types:
# Reset mocks for each test case
mock_email_service_instance = mock_external_service_dependencies["email_service_instance"]
mock_email_service_instance.reset_mock()
mock_email_service_instance.send_email.side_effect = exception
# Mock logging to capture error messages
with patch("tasks.mail_email_code_login.logger") as mock_logger:
# Act: Execute the task - it should handle the exception gracefully
send_email_code_login_mail_task(
language=test_language,
to=test_email,
code=test_code,
)
# Assert: Verify error handling
# Verify email service was called (and failed)
mock_email_service_instance.send_email.assert_called_once()
# Verify error was logged
error_calls = [
call
for call in mock_logger.exception.call_args_list
if f"Send email code login mail to {test_email} failed" in str(call)
]
# Check if any exception call was made (the exact message format may vary)
assert mock_logger.exception.call_count >= 1, f"Error should be logged for {error_name}"
# Reset side effect for next iteration
mock_email_service_instance.send_email.side_effect = None