您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

conftest.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. """
  2. TestContainers-based integration test configuration for Dify API.
  3. This module provides containerized test infrastructure using TestContainers library
  4. to spin up real database and service instances for integration testing. This approach
  5. ensures tests run against actual service implementations rather than mocks, providing
  6. more reliable and realistic test scenarios.
  7. """
  8. import logging
  9. import os
  10. from collections.abc import Generator
  11. from typing import Optional
  12. import pytest
  13. from flask import Flask
  14. from flask.testing import FlaskClient
  15. from sqlalchemy.orm import Session
  16. from testcontainers.core.container import DockerContainer
  17. from testcontainers.core.waiting_utils import wait_for_logs
  18. from testcontainers.postgres import PostgresContainer
  19. from testcontainers.redis import RedisContainer
  20. from app_factory import create_app
  21. from models import db
  22. # Configure logging for test containers
  23. logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
  24. logger = logging.getLogger(__name__)
  25. class DifyTestContainers:
  26. """
  27. Manages all test containers required for Dify integration tests.
  28. This class provides a centralized way to manage multiple containers
  29. needed for comprehensive integration testing, including databases,
  30. caches, and search engines.
  31. """
  32. def __init__(self):
  33. """Initialize container management with default configurations."""
  34. self.postgres: Optional[PostgresContainer] = None
  35. self.redis: Optional[RedisContainer] = None
  36. self.dify_sandbox: Optional[DockerContainer] = None
  37. self._containers_started = False
  38. logger.info("DifyTestContainers initialized - ready to manage test containers")
  39. def start_containers_with_env(self) -> None:
  40. """
  41. Start all required containers for integration testing.
  42. This method initializes and starts PostgreSQL, Redis
  43. containers with appropriate configurations for Dify testing. Containers
  44. are started in dependency order to ensure proper initialization.
  45. """
  46. if self._containers_started:
  47. logger.info("Containers already started - skipping container startup")
  48. return
  49. logger.info("Starting test containers for Dify integration tests...")
  50. # Start PostgreSQL container for main application database
  51. # PostgreSQL is used for storing user data, workflows, and application state
  52. logger.info("Initializing PostgreSQL container...")
  53. self.postgres = PostgresContainer(
  54. image="postgres:16-alpine",
  55. )
  56. self.postgres.start()
  57. db_host = self.postgres.get_container_host_ip()
  58. db_port = self.postgres.get_exposed_port(5432)
  59. os.environ["DB_HOST"] = db_host
  60. os.environ["DB_PORT"] = str(db_port)
  61. os.environ["DB_USERNAME"] = self.postgres.username
  62. os.environ["DB_PASSWORD"] = self.postgres.password
  63. os.environ["DB_DATABASE"] = self.postgres.dbname
  64. logger.info(
  65. "PostgreSQL container started successfully - Host: %s, Port: %s User: %s, Database: %s",
  66. db_host,
  67. db_port,
  68. self.postgres.username,
  69. self.postgres.dbname,
  70. )
  71. # Wait for PostgreSQL to be ready
  72. logger.info("Waiting for PostgreSQL to be ready to accept connections...")
  73. wait_for_logs(self.postgres, "is ready to accept connections", timeout=30)
  74. logger.info("PostgreSQL container is ready and accepting connections")
  75. # Install uuid-ossp extension for UUID generation
  76. logger.info("Installing uuid-ossp extension...")
  77. try:
  78. import psycopg2
  79. conn = psycopg2.connect(
  80. host=db_host,
  81. port=db_port,
  82. user=self.postgres.username,
  83. password=self.postgres.password,
  84. database=self.postgres.dbname,
  85. )
  86. conn.autocommit = True
  87. cursor = conn.cursor()
  88. cursor.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp";')
  89. cursor.close()
  90. conn.close()
  91. logger.info("uuid-ossp extension installed successfully")
  92. except Exception as e:
  93. logger.warning("Failed to install uuid-ossp extension: %s", e)
  94. # Set up storage environment variables
  95. os.environ["STORAGE_TYPE"] = "opendal"
  96. os.environ["OPENDAL_SCHEME"] = "fs"
  97. os.environ["OPENDAL_FS_ROOT"] = "storage"
  98. # Start Redis container for caching and session management
  99. # Redis is used for storing session data, cache entries, and temporary data
  100. logger.info("Initializing Redis container...")
  101. self.redis = RedisContainer(image="redis:latest", port=6379)
  102. self.redis.start()
  103. redis_host = self.redis.get_container_host_ip()
  104. redis_port = self.redis.get_exposed_port(6379)
  105. os.environ["REDIS_HOST"] = redis_host
  106. os.environ["REDIS_PORT"] = str(redis_port)
  107. logger.info("Redis container started successfully - Host: %s, Port: %s", redis_host, redis_port)
  108. # Wait for Redis to be ready
  109. logger.info("Waiting for Redis to be ready to accept connections...")
  110. wait_for_logs(self.redis, "Ready to accept connections", timeout=30)
  111. logger.info("Redis container is ready and accepting connections")
  112. # Start Dify Sandbox container for code execution environment
  113. # Dify Sandbox provides a secure environment for executing user code
  114. logger.info("Initializing Dify Sandbox container...")
  115. self.dify_sandbox = DockerContainer(image="langgenius/dify-sandbox:latest")
  116. self.dify_sandbox.with_exposed_ports(8194)
  117. self.dify_sandbox.env = {
  118. "API_KEY": "test_api_key",
  119. }
  120. self.dify_sandbox.start()
  121. sandbox_host = self.dify_sandbox.get_container_host_ip()
  122. sandbox_port = self.dify_sandbox.get_exposed_port(8194)
  123. os.environ["CODE_EXECUTION_ENDPOINT"] = f"http://{sandbox_host}:{sandbox_port}"
  124. os.environ["CODE_EXECUTION_API_KEY"] = "test_api_key"
  125. logger.info("Dify Sandbox container started successfully - Host: %s, Port: %s", sandbox_host, sandbox_port)
  126. # Wait for Dify Sandbox to be ready
  127. logger.info("Waiting for Dify Sandbox to be ready to accept connections...")
  128. wait_for_logs(self.dify_sandbox, "config init success", timeout=60)
  129. logger.info("Dify Sandbox container is ready and accepting connections")
  130. self._containers_started = True
  131. logger.info("All test containers started successfully")
  132. def stop_containers(self) -> None:
  133. """
  134. Stop and clean up all test containers.
  135. This method ensures proper cleanup of all containers to prevent
  136. resource leaks and conflicts between test runs.
  137. """
  138. if not self._containers_started:
  139. logger.info("No containers to stop - containers were not started")
  140. return
  141. logger.info("Stopping and cleaning up test containers...")
  142. containers = [self.redis, self.postgres, self.dify_sandbox]
  143. for container in containers:
  144. if container:
  145. try:
  146. container_name = container.image
  147. logger.info("Stopping container: %s", container_name)
  148. container.stop()
  149. logger.info("Successfully stopped container: %s", container_name)
  150. except Exception as e:
  151. # Log error but don't fail the test cleanup
  152. logger.warning("Failed to stop container %s: %s", container, e)
  153. self._containers_started = False
  154. logger.info("All test containers stopped and cleaned up successfully")
  155. # Global container manager instance
  156. _container_manager = DifyTestContainers()
  157. def _create_app_with_containers() -> Flask:
  158. """
  159. Create Flask application configured to use test containers.
  160. This function creates a Flask application instance that is configured
  161. to connect to the test containers instead of the default development
  162. or production databases.
  163. Returns:
  164. Flask: Configured Flask application for containerized testing
  165. """
  166. logger.info("Creating Flask application with test container configuration...")
  167. # Re-create the config after environment variables have been set
  168. from configs import dify_config
  169. # Force re-creation of config with new environment variables
  170. dify_config.__dict__.clear()
  171. dify_config.__init__()
  172. # Create and configure the Flask application
  173. logger.info("Initializing Flask application...")
  174. app = create_app()
  175. logger.info("Flask application created successfully")
  176. # Initialize database schema
  177. logger.info("Creating database schema...")
  178. with app.app_context():
  179. db.create_all()
  180. logger.info("Database schema created successfully")
  181. logger.info("Flask application configured and ready for testing")
  182. return app
  183. @pytest.fixture(scope="session")
  184. def set_up_containers_and_env() -> Generator[DifyTestContainers, None, None]:
  185. """
  186. Session-scoped fixture to manage test containers.
  187. This fixture ensures containers are started once per test session
  188. and properly cleaned up when all tests are complete. This approach
  189. improves test performance by reusing containers across multiple tests.
  190. Yields:
  191. DifyTestContainers: Container manager instance
  192. """
  193. logger.info("=== Starting test session container management ===")
  194. _container_manager.start_containers_with_env()
  195. logger.info("Test containers ready for session")
  196. yield _container_manager
  197. logger.info("=== Cleaning up test session containers ===")
  198. _container_manager.stop_containers()
  199. logger.info("Test session container cleanup completed")
  200. @pytest.fixture(scope="session")
  201. def flask_app_with_containers(set_up_containers_and_env) -> Flask:
  202. """
  203. Session-scoped Flask application fixture using test containers.
  204. This fixture provides a Flask application instance that is configured
  205. to use the test containers for all database and service connections.
  206. Args:
  207. containers: Container manager fixture
  208. Returns:
  209. Flask: Configured Flask application
  210. """
  211. logger.info("=== Creating session-scoped Flask application ===")
  212. app = _create_app_with_containers()
  213. logger.info("Session-scoped Flask application created successfully")
  214. return app
  215. @pytest.fixture
  216. def flask_req_ctx_with_containers(flask_app_with_containers) -> Generator[None, None, None]:
  217. """
  218. Request context fixture for containerized Flask application.
  219. This fixture provides a Flask request context for tests that need
  220. to interact with the Flask application within a request scope.
  221. Args:
  222. flask_app_with_containers: Flask application fixture
  223. Yields:
  224. None: Request context is active during yield
  225. """
  226. logger.debug("Creating Flask request context...")
  227. with flask_app_with_containers.test_request_context():
  228. logger.debug("Flask request context active")
  229. yield
  230. logger.debug("Flask request context closed")
  231. @pytest.fixture
  232. def test_client_with_containers(flask_app_with_containers) -> Generator[FlaskClient, None, None]:
  233. """
  234. Test client fixture for containerized Flask application.
  235. This fixture provides a Flask test client that can be used to make
  236. HTTP requests to the containerized application for integration testing.
  237. Args:
  238. flask_app_with_containers: Flask application fixture
  239. Yields:
  240. FlaskClient: Test client instance
  241. """
  242. logger.debug("Creating Flask test client...")
  243. with flask_app_with_containers.test_client() as client:
  244. logger.debug("Flask test client ready")
  245. yield client
  246. logger.debug("Flask test client closed")
  247. @pytest.fixture
  248. def db_session_with_containers(flask_app_with_containers) -> Generator[Session, None, None]:
  249. """
  250. Database session fixture for containerized testing.
  251. This fixture provides a SQLAlchemy database session that is connected
  252. to the test PostgreSQL container, allowing tests to interact with
  253. the database directly.
  254. Args:
  255. flask_app_with_containers: Flask application fixture
  256. Yields:
  257. Session: Database session instance
  258. """
  259. logger.debug("Creating database session...")
  260. with flask_app_with_containers.app_context():
  261. session = db.session()
  262. logger.debug("Database session created and ready")
  263. try:
  264. yield session
  265. finally:
  266. session.close()
  267. logger.debug("Database session closed")