Skip to content

Commit 7031fe9

Browse files
committed
Introduce server fixture to run app in subprocess for Socket.IO integration tests and update sync scenarios to use it.
1 parent 3079b30 commit 7031fe9

5 files changed

Lines changed: 84 additions & 17 deletions

File tree

297 Bytes
Binary file not shown.
2.86 KB
Binary file not shown.
Binary file not shown.

server/tests/conftest.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
44
from sqlalchemy.orm import sessionmaker
55
from app.database import Base, get_db
6-
from app.main import fastapi_app as app
6+
from app.main import app, fastapi_app
77
import asyncio
88
from fastapi.testclient import TestClient
99

@@ -47,10 +47,79 @@ def event_loop():
4747
def override_dependency(test_db):
4848
async def _get_db_override():
4949
yield test_db
50-
app.dependency_overrides[get_db] = _get_db_override
50+
fastapi_app.dependency_overrides[get_db] = _get_db_override
51+
52+
# Also override SessionLocal in main.py for Socket.IO handlers
53+
import app.main
54+
app.main.SessionLocal = TestingSessionLocal
5155

5256
@pytest.fixture
5357
def client():
5458
with TestClient(app) as c:
5559
yield c
5660

61+
@pytest_asyncio.fixture(scope="session")
62+
async def server():
63+
"""Starts a real uvicorn server for Socket.IO tests using subprocess"""
64+
import subprocess
65+
import time
66+
import socket
67+
import os
68+
from httpx import AsyncClient
69+
70+
# Find a free port
71+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
72+
sock.bind(('127.0.0.1', 0))
73+
port = sock.getsockname()[1]
74+
sock.close()
75+
76+
# Use the same database for the subprocess
77+
# Since we are using in-memory SQLite, this won't work across processes!
78+
# We should use a temporary file-based database for these tests.
79+
test_db_file = "test_sync.db"
80+
if os.path.exists(test_db_file):
81+
os.remove(test_db_file)
82+
83+
db_url = f"sqlite+aiosqlite:///./{test_db_file}"
84+
85+
cmd = [
86+
"uv", "run", "uvicorn", "app.main:app",
87+
"--host", "127.0.0.1",
88+
"--port", str(port),
89+
"--log-level", "error"
90+
]
91+
92+
env = os.environ.copy()
93+
env["PYTHONPATH"] = "."
94+
env["DATABASE_URL"] = db_url
95+
96+
process = subprocess.Popen(cmd, env=env, cwd=os.getcwd())
97+
98+
base_url = f"http://127.0.0.1:{port}"
99+
100+
# Wait for server to be ready
101+
max_retries = 50
102+
for _ in range(max_retries):
103+
try:
104+
async with AsyncClient() as client:
105+
res = await client.get(f"{base_url}/health")
106+
if res.status_code == 200:
107+
break
108+
except Exception:
109+
pass
110+
await asyncio.sleep(0.2)
111+
112+
yield base_url
113+
114+
process.terminate()
115+
try:
116+
process.wait(timeout=5)
117+
except subprocess.TimeoutExpired:
118+
process.kill()
119+
120+
if os.path.exists(test_db_file):
121+
try:
122+
os.remove(test_db_file)
123+
except:
124+
pass
125+

server/tests/test_sync_scenarios.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@
55
from httpx import AsyncClient, ASGITransport
66
from app.main import app
77

8-
BASE_URL = "http://localhost:8000"
9-
108
@pytest.mark.asyncio
11-
async def test_sync_and_persistence():
9+
async def test_sync_and_persistence(server):
1210
# 1. Create Session
13-
async with AsyncClient(base_url=BASE_URL) as ac:
11+
async with AsyncClient(base_url=server) as ac:
1412
res = await ac.post("/sessions", json={
1513
"candidateName": "Test Candidate",
1614
"candidateEmail": "test@example.com",
@@ -31,7 +29,7 @@ async def on_session_updated(data):
3129
if not future_session_update.done():
3230
future_session_update.set_result(data)
3331

34-
await sio_a.connect(BASE_URL, socketio_path='/socket.io')
32+
await sio_a.connect(server, socketio_path='/socket.io')
3533
await sio_a.emit('join_room', {
3634
'roomId': session_id,
3735
'user': {'id': 'user_a', 'name': 'Interviewer', 'role': 'interviewer'}
@@ -67,7 +65,7 @@ async def on_code_change(data):
6765
if not future_code_sync.done():
6866
future_code_sync.set_result(data)
6967

70-
await sio_b.connect(BASE_URL, socketio_path='/socket.io')
68+
await sio_b.connect(server, socketio_path='/socket.io')
7169
await sio_b.emit('join_room', {
7270
'roomId': session_id,
7371
'user': {'id': 'user_b', 'name': 'Candidate', 'role': 'candidate'}
@@ -80,9 +78,9 @@ async def on_code_change(data):
8078
await sio_b.disconnect()
8179

8280
@pytest.mark.asyncio
83-
async def test_user_presence():
81+
async def test_user_presence(server):
8482
# 1. Create Session
85-
async with AsyncClient(base_url=BASE_URL) as ac:
83+
async with AsyncClient(base_url=server) as ac:
8684
res = await ac.post("/sessions", json={
8785
"candidateName": "Test Candidate",
8886
"candidateEmail": "test@example.com",
@@ -102,14 +100,14 @@ async def on_room_users_b(data):
102100
future_users_b.set_result(data)
103101

104102
# A joins
105-
await sio_a.connect(BASE_URL, socketio_path='/socket.io')
103+
await sio_a.connect(server, socketio_path='/socket.io')
106104
await sio_a.emit('join_room', {
107105
'roomId': session_id,
108106
'user': {'id': 'user_a', 'name': 'Interviewer', 'role': 'interviewer'}
109107
})
110108

111109
# B joins
112-
await sio_b.connect(BASE_URL, socketio_path='/socket.io')
110+
await sio_b.connect(server, socketio_path='/socket.io')
113111
await sio_b.emit('join_room', {
114112
'roomId': session_id,
115113
'user': {'id': 'user_b', 'name': 'Candidate', 'role': 'candidate'}
@@ -123,7 +121,7 @@ async def on_room_users_b(data):
123121
future_users_b_leave = asyncio.Future()
124122
@sio_b.on('room_users')
125123
async def on_room_users_b_leave(data):
126-
if not future_users_b_leave.done():
124+
if len(data['users']) == 1 and not future_users_b_leave.done():
127125
future_users_b_leave.set_result(data)
128126

129127
await sio_a.emit('leave_room', {'roomId': session_id, 'userId': 'user_a'})
@@ -137,9 +135,9 @@ async def on_room_users_b_leave(data):
137135
await sio_b.disconnect()
138136

139137
@pytest.mark.asyncio
140-
async def test_whiteboard_sync():
138+
async def test_whiteboard_sync(server):
141139
# 1. Create Session
142-
async with AsyncClient(base_url=BASE_URL) as ac:
140+
async with AsyncClient(base_url=server) as ac:
143141
res = await ac.post("/sessions", json={
144142
"candidateName": "Test Candidate",
145143
"candidateEmail": "test@example.com",
@@ -159,13 +157,13 @@ async def on_whiteboard_update(data):
159157
future_wb_update.set_result(data)
160158

161159
# Connect both
162-
await sio_a.connect(BASE_URL, socketio_path='/socket.io')
160+
await sio_a.connect(server, socketio_path='/socket.io')
163161
await sio_a.emit('join_room', {
164162
'roomId': session_id,
165163
'user': {'id': 'user_a', 'name': 'Interviewer', 'role': 'interviewer'}
166164
})
167165

168-
await sio_b.connect(BASE_URL, socketio_path='/socket.io')
166+
await sio_b.connect(server, socketio_path='/socket.io')
169167
await sio_b.emit('join_room', {
170168
'roomId': session_id,
171169
'user': {'id': 'user_b', 'name': 'Candidate', 'role': 'candidate'}

0 commit comments

Comments
 (0)