File size: 11,499 Bytes
aef804e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
"""
Integration test fixtures with FastAPI TestClient setup.

Provides database sessions, TestClient with dependency overrides,
and authentication fixtures for API testing.
"""

import os
import pytest
import uuid
import tempfile
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker

# Set TESTING environment variable BEFORE any imports
os.environ["TESTING"] = "1"

# Global monkeypatch for SQLite compatibility: redirect JSONB to JSON
try:
    from sqlalchemy.dialects.postgresql import JSONB
    import sqlalchemy.dialects.postgresql as postgresql
    import sqlalchemy.types as types
    postgresql.JSONB = types.JSON
except ImportError:
    pass

# Add parent directory to path for imports
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))

from main_api_app import app
from core.auth import create_access_token
from core.database import get_db, Base
from core.models import User, AgentRegistry, AgentMemory, TokenUsage, AgentExecution, AgentStatus, ExecutionStatus
from tests.factories.user_factory import AdminUserFactory


@pytest.fixture(scope="function")
def db_session():
    """
    Create a fresh in-memory database for each test.

    Simplified version that avoids sorted_tables to prevent NoReferencedTableError
    when running multiple tests in sequence.
    """
    # Use file-based temp SQLite for tests
    fd, db_path = tempfile.mkstemp(suffix='.db')
    os.close(fd)

    engine = create_engine(
        f"sqlite:///{db_path}",
        connect_args={"check_same_thread": False},
        echo=False
    )

    # Store path for cleanup
    engine._test_db_path = db_path

    print(f"DEBUG: Tables in metadata: {list(Base.metadata.tables.keys())}")

    # Create tables
    print(f"DEBUG: Creating {len(Base.metadata.tables)} tables: {list(Base.metadata.tables.keys())}")
    Base.metadata.create_all(engine)

    # Create session
    TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=False)
    
    # Global Patching: ensure all services use the same engine/session
    import core.database
    original_engine = core.database.engine
    original_sessionlocal = core.database.SessionLocal
    core.database.engine = engine
    core.database.SessionLocal = TestingSessionLocal
    
    session = TestingSessionLocal()

    yield session

    # Cleanup
    session.close()
    engine.dispose()
    # Delete temp database file
    if hasattr(engine, '_test_db_path'):
        try:
            os.unlink(engine._test_db_path)
        except Exception:
            pass


@pytest.fixture(scope="function")
def client(db_session: Session):
    """
    Create TestClient with dependency override for test database.

    This fixture overrides the get_db dependency to use the test database
    session, ensuring all API requests use the test database with transaction
    rollback for isolation. Also bypasses authentication for integration tests.
    """
    def _get_db():
        try:
            yield db_session
        finally:
            pass  # Transaction rolls back

    app.dependency_overrides[get_db] = _get_db

    # Override get_current_user to bypass auth - create lazy user factory
    def _mock_get_current_user():
        """Mock get_current_user - creates or returns test user with admin role"""
        from tests.factories.user_factory import AdminUserFactory

        # Use unique email to avoid conflicts
        unique_id = str(uuid.uuid4())[:8]
        email = f"test_{unique_id}@integration.com"

        # Try to get existing user, handling case where tables don't exist yet
        try:
            user = db_session.query(User).filter(User.email == email).first()
            if user:
                return user
        except Exception as e:
            # Table doesn't exist or other error, will create user below
            pass

        # Create new admin user with all permissions
        user = AdminUserFactory(email=email, _session=db_session)
        db_session.commit()
        db_session.refresh(user)

        return user

    # Override get_current_user to bypass auth
    # Import from core.auth where it's actually defined
    try:
        from core.auth import get_current_user
        app.dependency_overrides[get_current_user] = _mock_get_current_user
    except ImportError:
        pass

    # Modify TrustedHostMiddleware to allow testserver
    # The middleware is stored as Middleware objects with cls and kwargs attributes
    for middleware in app.user_middleware:
        if hasattr(middleware, 'cls') and middleware.cls.__name__ == 'TrustedHostMiddleware':
            # Modify the allowed_hosts to include testserver
            middleware.kwargs['allowed_hosts'] = ['testserver', 'localhost', '127.0.0.1', '0.0.0.0', '*']
            break

    # Create TestClient with proper headers
    test_client = TestClient(app, base_url="http://testserver")

    yield test_client

    app.dependency_overrides.clear()


@pytest.fixture(scope="function")
def client_no_auth(db_session: Session):
    """
    Create TestClient WITHOUT bypassing authentication.

    This fixture provides a client that enforces authentication,
    useful for testing auth requirements and permissions.
    """
    def _get_db():
        try:
            yield db_session
        finally:
            pass  # Transaction rolls back

    app.dependency_overrides[get_db] = _get_db

    # Do NOT override get_current_user - authentication is enforced

    # Modify TrustedHostMiddleware to allow testserver
    for middleware in app.user_middleware:
        if hasattr(middleware, 'cls') and middleware.cls.__name__ == 'TrustedHostMiddleware':
            middleware.kwargs['allowed_hosts'] = ['testserver', 'localhost', '127.0.0.1', '0.0.0.0', '*']
            break

    # Create TestClient with proper headers
    test_client = TestClient(app, base_url="http://testserver")

    yield test_client

    app.dependency_overrides.clear()


@pytest.fixture(scope="function")
def auth_token(db_session: Session):
    """
    Create valid JWT token for test user.

    Creates a test user in the database and returns a JWT token
    that can be used for authenticated requests.
    """
    from tests.factories.user_factory import UserFactory
    unique_id = str(uuid.uuid4())[:8]
    user = UserFactory(email=f"auth_{unique_id}@integration.com", _session=db_session)
    db_session.add(user)
    db_session.commit()
    db_session.refresh(user)

    token = create_access_token(data={"sub": user.id})
    return token


@pytest.fixture(scope="function")
def admin_token(db_session: Session):
    """
    Create JWT token for admin user.

    Creates an admin user in the database and returns a JWT token
    with admin privileges for testing admin-only endpoints.
    """
    from tests.factories.user_factory import AdminUserFactory
    unique_id = str(uuid.uuid4())[:8]
    admin = AdminUserFactory(email=f"admin_{unique_id}@integration.com", _session=db_session)
    db_session.add(admin)
    db_session.commit()
    db_session.refresh(admin)

    token = create_access_token(data={"sub": admin.id})
    return token


@pytest.fixture(scope="function")
def test_user(db_session: Session):
    """
    Create a test user in the database.

    Returns a User instance that can be used for testing
    user-related endpoints.
    """
    from tests.factories.user_factory import UserFactory
    unique_id = str(uuid.uuid4())[:8]
    user = UserFactory(email=f"user_{unique_id}@integration.com", _session=db_session)
    db_session.add(user)
    db_session.commit()
    db_session.refresh(user)
    return user


@pytest.fixture(scope="function")
def auth_headers(auth_token: str):
    """
    Create authentication headers for API requests.

    Returns a dictionary with Authorization header set to Bearer token.
    """
    return {"Authorization": f"Bearer {auth_token}"}


@pytest.fixture(scope="function")
def admin_headers(admin_token: str):
    """
    Create admin authentication headers for API requests.

    Returns a dictionary with Authorization header set to admin Bearer token.
    """
    return {"Authorization": f"Bearer {admin_token}"}


# ============================================================================
# E2E Test Fixtures for Agent Execution Workflow
# ============================================================================

@pytest.fixture(scope="function")
def e2e_db_session(db_session: Session):
    """
    E2E database session with aggressive cleanup.

    Cleans up all E2E test data after each test to prevent cross-test contamination.
    """
    yield db_session

    # Aggressive cleanup for E2E tests
    try:
        # Clean up in order of dependencies
        from sqlalchemy import text
        db_session.execute(text("DELETE FROM episode_segments WHERE 1=1"))
        db_session.execute(text("DELETE FROM agent_episodes WHERE agent_id LIKE 'test-agent%'"))
        db_session.execute(text("DELETE FROM agent_executions WHERE agent_id LIKE 'test-agent%'"))
        db_session.execute(text("DELETE FROM agent_registry WHERE id LIKE 'test-agent%'"))
        db_session.commit()
    except Exception as e:
        db_session.rollback()
        print(f"E2E cleanup error: {e}")


@pytest.fixture(scope="function")
def mock_llm_streaming():
    """
    Mock LLM streaming response for E2E tests.

    Returns an async generator that yields streaming chunks.
    """
    async def stream_completion(*args, **kwargs):
        """Mock streaming completion with test response."""
        chunks = [
            "Test ",
            "response ",
            "chunk 1",
            "Test ",
            "response ",
            "chunk 2",
            "Test ",
            "response ",
            "chunk 3"
        ]
        for chunk in chunks:
            yield {
                "choices": [{
                    "delta": {"content": chunk},
                    "finish_reason": None
                }],
                "usage": None
            }
        # Final chunk with finish_reason
        yield {
            "choices": [{
                "delta": {},
                "finish_reason": "stop"
            }],
            "usage": {
                "prompt_tokens": 10,
                "completion_tokens": 20,
                "total_tokens": 30
            }
        }

    return stream_completion


@pytest.fixture(scope="function")
def mock_llm_streaming_error():
    """
    Mock LLM streaming error for E2E error path tests.
    """
    async def stream_completion_error(*args, **kwargs):
        """Mock streaming completion with error."""
        yield {
            "choices": [{
                "delta": {"content": "Initial chunk"},
                "finish_reason": None
            }],
            "usage": None
        }
        # Simulate LLM API error
        raise Exception("LLM API error: rate limit exceeded")

    return stream_completion_error


@pytest.fixture(scope="function")
def e2e_client(client, e2e_db_session, mock_websocket):
    """
    E2E test client with all necessary mocks.

    Combines TestClient with database session, WebSocket mocks,
    and authentication bypass for comprehensive E2E testing.
    """
    yield client


@pytest.fixture(scope="function")
def execution_id():
    """
    Generate unique execution ID for E2E tests.
    """
    import uuid
    return str(uuid.uuid4())