app.bettersight.io/backend/tests/services/test_jobs.py

154 lines
6.7 KiB
Python

"""
jobs.run_research_job — per-competitor failure isolation, status
transitions, and the slow/stuck Gotify alert thresholds from §29.
tests/ is the conftest-adjacent location; this file lives under
tests/services/ purely for directory tidiness (jobs.py itself is a
top-level module, not part of services/).
"""
import jobs
from repositories.scrape_repository import scrape_repository
from repositories.competitor_repository import competitor_repository
from repositories.tenant_repository import tenant_repository
def _setup(max_seats=5):
tenant = tenant_repository.create({
'name': 'G Adventures', 'domain': 'gadventures.com', 'tier': 'analyse',
'status': 'active', 'max_seats': max_seats,
})
competitor = competitor_repository.create({
'tenant_id': tenant['id'], 'name': 'Intrepid Travel', 'website': 'https://intrepidtravel.com',
'catalogue_url': 'https://intrepidtravel.com/adventures', 'primary_market': 'GB', 'active': True,
})
run = scrape_repository.create({
'tenant_id': tenant['id'], 'triggered_by': 'manual', 'status': 'pending',
'competitors_total': 1, 'competitors_done': 0, 'results': {},
})
return tenant, competitor, run
def test_run_research_job_marks_complete_on_success(monkeypatch):
tenant, competitor, run = _setup()
monkeypatch.setattr(
'services.analysis_service.run_competitor_analysis',
lambda tenant_id, comp, ctx: {'path': 'fast', 'result': {'tripName': 'Peru Classic'}}
)
gotify_calls = []
monkeypatch.setattr('services.alert_service.send_gotify', lambda **kw: gotify_calls.append(kw))
jobs.run_research_job(run['id'], {'tenant_id': tenant['id'], 'competitors': [{'id': competitor['id']}]})
updated = scrape_repository.get_by_id(run['id'])
assert updated['status'] == 'complete'
assert updated['competitors_done'] == 1
assert updated['results']['success'][0]['name'] == 'Intrepid Travel'
assert gotify_calls == [] # fast completion, no slow/stuck alert
def test_run_research_job_isolates_one_competitor_failure(monkeypatch):
tenant = tenant_repository.create({
'name': 'G Adventures', 'domain': 'gadventures.com', 'tier': 'analyse',
'status': 'active', 'max_seats': 5,
})
good = competitor_repository.create({
'tenant_id': tenant['id'], 'name': 'Exodus', 'website': 'https://exodustravels.com',
'catalogue_url': 'https://exodustravels.com/trips', 'primary_market': 'GB', 'active': True,
})
bad = competitor_repository.create({
'tenant_id': tenant['id'], 'name': 'Flash Pack', 'website': 'https://flashpack.com',
'catalogue_url': 'https://flashpack.com/adventures', 'primary_market': 'US', 'active': True,
})
run = scrape_repository.create({
'tenant_id': tenant['id'], 'triggered_by': 'manual', 'status': 'pending',
'competitors_total': 2, 'competitors_done': 0, 'results': {},
})
def _fake_analysis(tenant_id, comp, ctx):
if comp['name'] == 'Flash Pack':
raise ValueError('scrape_blocked')
return {'path': 'fast', 'result': {}}
monkeypatch.setattr('services.analysis_service.run_competitor_analysis', _fake_analysis)
monkeypatch.setattr('services.alert_service.send_gotify', lambda **kw: None)
jobs.run_research_job(run['id'], {
'tenant_id': tenant['id'], 'competitors': [{'id': good['id']}, {'id': bad['id']}]
})
updated = scrape_repository.get_by_id(run['id'])
assert updated['status'] == 'complete' # at least one success → overall complete
assert len(updated['results']['success']) == 1
assert len(updated['results']['failed']) == 1
assert updated['results']['failed'][0]['name'] == 'Flash Pack'
def test_run_research_job_marks_failed_when_all_competitors_fail(monkeypatch):
tenant, competitor, run = _setup()
monkeypatch.setattr(
'services.analysis_service.run_competitor_analysis',
lambda *a, **k: (_ for _ in ()).throw(ValueError('extract_failed'))
)
gotify_calls = []
monkeypatch.setattr('services.alert_service.send_gotify', lambda **kw: gotify_calls.append(kw))
jobs.run_research_job(run['id'], {'tenant_id': tenant['id'], 'competitors': [{'id': competitor['id']}]})
updated = scrape_repository.get_by_id(run['id'])
assert updated['status'] == 'failed'
assert any('Analysis job failed' in c['title'] for c in gotify_calls)
def test_run_research_job_fires_slow_alert_past_threshold(monkeypatch):
tenant, competitor, run = _setup()
monkeypatch.setattr(
'services.analysis_service.run_competitor_analysis',
lambda *a, **k: {'path': 'fast', 'result': {}}
)
gotify_calls = []
monkeypatch.setattr('services.alert_service.send_gotify', lambda **kw: gotify_calls.append(kw))
# Force the elapsed-time calculation past the slow threshold without
# an actual 5-minute sleep — patch time.time() to jump forward
# between the job's start and end reads.
real_time = jobs.time.time
times = iter([real_time(), real_time() + jobs.JOB_SLOW_SECONDS + 5])
monkeypatch.setattr(jobs.time, 'time', lambda: next(times))
jobs.run_research_job(run['id'], {'tenant_id': tenant['id'], 'competitors': [{'id': competitor['id']}]})
assert any('Slow job detected' in c['title'] for c in gotify_calls)
def test_run_research_job_handles_unknown_competitor_id(monkeypatch):
tenant = tenant_repository.create({
'name': 'G Adventures', 'domain': 'gadventures.com', 'tier': 'analyse',
'status': 'active', 'max_seats': 5,
})
run = scrape_repository.create({
'tenant_id': tenant['id'], 'triggered_by': 'manual', 'status': 'pending',
'competitors_total': 1, 'competitors_done': 0, 'results': {},
})
monkeypatch.setattr('services.alert_service.send_gotify', lambda **kw: None)
jobs.run_research_job(run['id'], {'tenant_id': tenant['id'], 'competitors': [{'id': 'does-not-exist'}]})
updated = scrape_repository.get_by_id(run['id'])
assert updated['status'] == 'failed'
assert updated['results']['failed'][0]['error'] == 'Competitor not found'
def test_enqueue_research_job_returns_job_id(monkeypatch):
captured = {}
monkeypatch.setattr(
jobs.normal_q, 'enqueue',
lambda fn, job_id, data, job_id_kw=None, **kw: captured.update(job_id=job_id, data=data)
)
# rq's Queue.enqueue signature takes job_id as a kwarg named "job_id";
# patch it generically to capture call args regardless of exact form.
monkeypatch.setattr(jobs.normal_q, 'enqueue', lambda *a, **k: captured.update(args=a, kwargs=k))
result = jobs.enqueue_research_job('job123', {'tenant_id': 't1'})
assert result == 'job123'
assert captured['kwargs']['job_id'] == 'job123'