You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
SoulSync/tests/test_phase3_enrichment.py

230 lines
8.8 KiB

"""Phase 3 WebSocket migration tests — Enrichment sidebar workers.
Verifies that:
- All 7 enrichment worker statuses are delivered identically via
WebSocket events and HTTP endpoints
- Each worker's data shape is correct
- HTTP endpoints still work as fallback
IMPORTANT: Do NOT use ``from tests.conftest import …`` — pytest's auto-discovered
conftest is a different module instance. Use the ``shared_state`` fixture instead.
"""
import pytest
# All 7 enrichment workers
WORKERS = [
'musicbrainz', 'audiodb', 'deezer',
'spotify-enrichment', 'itunes-enrichment',
'hydrabase', 'repair',
]
# Endpoint URLs keyed by worker name
ENDPOINTS = {
'musicbrainz': '/api/musicbrainz/status',
'audiodb': '/api/audiodb/status',
'deezer': '/api/deezer/status',
'spotify-enrichment': '/api/spotify-enrichment/status',
'itunes-enrichment': '/api/itunes-enrichment/status',
'hydrabase': '/api/hydrabase-worker/status',
'repair': '/api/repair/status',
}
# =========================================================================
# Group A — Event Delivery (parameterized)
# =========================================================================
class TestEnrichmentEventDelivery:
"""enrichment:<worker> socket events are received by the client."""
@pytest.mark.parametrize('worker', WORKERS)
def test_enrichment_event_received(self, test_app, shared_state, worker):
"""Client receives an enrichment:<worker> event."""
app, socketio = test_app
client = socketio.test_client(app)
build = shared_state['build_enrichment_status']
socketio.emit(f'enrichment:{worker}', build(worker))
received = client.get_received()
events = [e for e in received if e['name'] == f'enrichment:{worker}']
assert len(events) >= 1
# =========================================================================
# Group B — Data Shape (parameterized)
# =========================================================================
class TestEnrichmentDataShape:
"""enrichment:<worker> event data has the expected keys."""
@pytest.mark.parametrize('worker', [
'musicbrainz', 'audiodb', 'deezer',
'spotify-enrichment', 'itunes-enrichment',
])
def test_standard_enrichment_shape(self, test_app, shared_state, worker):
"""Standard enrichment worker data has running, paused, idle, progress."""
app, socketio = test_app
client = socketio.test_client(app)
build = shared_state['build_enrichment_status']
socketio.emit(f'enrichment:{worker}', build(worker))
received = client.get_received()
events = [e for e in received if e['name'] == f'enrichment:{worker}']
assert len(events) >= 1
data = events[0]['args'][0]
assert 'running' in data
assert 'paused' in data
assert 'idle' in data
assert 'current_item' in data
assert 'progress' in data
assert isinstance(data['running'], bool)
assert isinstance(data['paused'], bool)
def test_spotify_enrichment_has_authenticated(self, test_app, shared_state):
"""Spotify enrichment includes the 'authenticated' field."""
app, socketio = test_app
client = socketio.test_client(app)
build = shared_state['build_enrichment_status']
socketio.emit('enrichment:spotify-enrichment', build('spotify-enrichment'))
received = client.get_received()
events = [e for e in received if e['name'] == 'enrichment:spotify-enrichment']
data = events[0]['args'][0]
assert 'authenticated' in data
def test_hydrabase_shape(self, test_app, shared_state):
"""Hydrabase worker has running, paused, queue_size (no idle/progress)."""
app, socketio = test_app
client = socketio.test_client(app)
build = shared_state['build_enrichment_status']
socketio.emit('enrichment:hydrabase', build('hydrabase'))
received = client.get_received()
events = [e for e in received if e['name'] == 'enrichment:hydrabase']
data = events[0]['args'][0]
assert 'running' in data
assert 'paused' in data
assert 'queue_size' in data
assert 'idle' not in data # Hydrabase doesn't have idle
def test_repair_shape(self, test_app, shared_state):
"""Repair worker has progress.tracks with checked/repaired counters."""
app, socketio = test_app
client = socketio.test_client(app)
build = shared_state['build_enrichment_status']
socketio.emit('enrichment:repair', build('repair'))
received = client.get_received()
events = [e for e in received if e['name'] == 'enrichment:repair']
data = events[0]['args'][0]
assert 'running' in data
assert 'progress' in data
tracks = data['progress']['tracks']
assert 'checked' in tracks
assert 'total' in tracks
assert 'repaired' in tracks
# =========================================================================
# Group C — HTTP Parity (parameterized)
# =========================================================================
class TestEnrichmentHttpParity:
"""Socket event data matches HTTP endpoint response."""
@pytest.mark.parametrize('worker', WORKERS)
def test_enrichment_matches_http(self, test_app, shared_state, worker):
"""Socket event data matches GET /api/<worker>/status."""
app, socketio = test_app
flask_client = app.test_client()
ws_client = socketio.test_client(app)
build = shared_state['build_enrichment_status']
endpoint = ENDPOINTS[worker]
http_data = flask_client.get(endpoint).get_json()
socketio.emit(f'enrichment:{worker}', build(worker))
received = ws_client.get_received()
events = [e for e in received if e['name'] == f'enrichment:{worker}']
assert len(events) >= 1
ws_data = events[0]['args'][0]
# Both should have the same running/paused state
assert ws_data['running'] == http_data['running']
assert ws_data['paused'] == http_data['paused']
# =========================================================================
# Group D — HTTP Still Works (parameterized)
# =========================================================================
class TestEnrichmentHttpStillWorks:
"""HTTP endpoints return 200 with expected structure."""
@pytest.mark.parametrize('worker', WORKERS)
def test_http_enrichment_still_works(self, flask_client, worker):
"""GET /api/<worker>/status returns 200."""
endpoint = ENDPOINTS[worker]
resp = flask_client.get(endpoint)
assert resp.status_code == 200
data = resp.get_json()
assert 'running' in data
assert 'paused' in data
# =========================================================================
# Group E — Backward Compatibility
# =========================================================================
class TestEnrichmentBackwardCompat:
"""HTTP endpoints work when no WebSocket is connected."""
def test_all_http_endpoints_work_without_socket(self, flask_client):
"""All 7 enrichment HTTP endpoints work without any WebSocket connection."""
for worker in WORKERS:
endpoint = ENDPOINTS[worker]
resp = flask_client.get(endpoint)
assert resp.status_code == 200
data = resp.get_json()
assert data['running'] is True
def test_multiple_clients_get_enrichment_updates(self, test_app, shared_state):
"""Multiple WebSocket clients each receive enrichment events."""
app, socketio = test_app
client1 = socketio.test_client(app)
client2 = socketio.test_client(app)
build = shared_state['build_enrichment_status']
socketio.emit('enrichment:musicbrainz', build('musicbrainz'))
for client in [client1, client2]:
received = client.get_received()
events = [e for e in received if e['name'] == 'enrichment:musicbrainz']
assert len(events) >= 1
client1.disconnect()
client2.disconnect()
def test_enrichment_reflects_state_change(self, test_app, shared_state):
"""When enrichment state changes, the next emit reflects it."""
app, socketio = test_app
client = socketio.test_client(app)
enrich = shared_state['enrichment_status']
build = shared_state['build_enrichment_status']
# Mutate state
enrich['musicbrainz']['paused'] = True
enrich['musicbrainz']['running'] = False
socketio.emit('enrichment:musicbrainz', build('musicbrainz'))
received = client.get_received()
events = [e for e in received if e['name'] == 'enrichment:musicbrainz']
data = events[-1]['args'][0]
assert data['paused'] is True
assert data['running'] is False