Reload cleared the caches first and rebuilt afterwards, so any failure left the API serving nothing, and requests arriving mid-reload saw a half-swapped state. It now builds and validates the replacement frames, place registry, reverse index and sitemaps off the request loop, then publishes them in one synchronous step under a lock. A failed reload returns 503 and keeps the previous data. Sitemap regeneration takes the same path rather than clearing the live registry up front. Typesense search returned at most one page of hits and used an empty list for both "no matches" and "search is down", so a genuine empty result silently fell back to substring matching. It now pages through every candidate and returns None only on failure; the fallback matches literally, since a query containing regex metacharacters used to throw. Empty datasets answer 503 rather than 200-with-nothing or a misleading 404, so callers can tell an outage from an absent school. Adds /api/release, which reports the build identity baked into the image. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
74 lines
3.2 KiB
Python
74 lines
3.2 KiB
Python
from types import SimpleNamespace
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from backend import app as api, data_loader
|
|
from backend.tests.test_sixth_form_flag import _schools_df
|
|
|
|
|
|
def client_for(monkeypatch, search):
|
|
client = SimpleNamespace(collections={'schools': SimpleNamespace(documents=SimpleNamespace(search=search))})
|
|
monkeypatch.setattr(data_loader, '_get_typesense_client', lambda: client)
|
|
|
|
|
|
def test_search_returns_matches_beyond_first_page(monkeypatch):
|
|
pages = []
|
|
def search(params):
|
|
pages.append(params['page'])
|
|
urns = range(100000, 100250) if params['page'] == 1 else [100999]
|
|
return {'found': 251, 'hits': [{'document': {'urn': u}} for u in urns]}
|
|
client_for(monkeypatch, search)
|
|
result = data_loader.search_schools_typesense('academy')
|
|
assert len(result) == 251
|
|
assert result[-1] == 100999
|
|
assert pages == [1, 2]
|
|
|
|
|
|
def test_later_page_failure_does_not_return_partial_results(monkeypatch):
|
|
def search(params):
|
|
if params['page'] == 2:
|
|
raise RuntimeError('timeout')
|
|
return {'found': 251, 'hits': [{'document': {'urn': u}} for u in range(100000, 100250)]}
|
|
client_for(monkeypatch, search)
|
|
assert data_loader.search_schools_typesense('academy') is None
|
|
|
|
|
|
def test_zero_matches_are_distinct_from_unavailable(monkeypatch):
|
|
client_for(monkeypatch, lambda _: {'found': 0, 'hits': []})
|
|
assert data_loader.search_schools_typesense('academy') == []
|
|
monkeypatch.setattr(data_loader, '_get_typesense_client', lambda: None)
|
|
assert data_loader.search_schools_typesense('academy') is None
|
|
|
|
|
|
@pytest.mark.parametrize('matches, expected', [([], []), (None, [100001])])
|
|
def test_fallback_only_on_dependency_failure(monkeypatch, matches, expected):
|
|
monkeypatch.setattr(api.limiter, 'enabled', False)
|
|
monkeypatch.setattr(api, 'load_latest_school_data', _schools_df)
|
|
monkeypatch.setattr(api, 'search_schools_typesense', lambda _: matches)
|
|
response = TestClient(api.app).get('/api/schools?search=Alpha')
|
|
assert response.status_code == 200
|
|
assert [s['urn'] for s in response.json()['schools']] == expected
|
|
|
|
|
|
def test_filtered_api_keeps_match_from_second_search_page(monkeypatch):
|
|
monkeypatch.setattr(api.limiter, 'enabled', False)
|
|
df = _schools_df()
|
|
monkeypatch.setattr(api, 'load_latest_school_data', lambda: df)
|
|
def search(params):
|
|
urns = range(200000, 200250) if params['page'] == 1 else [100001]
|
|
return {'found': 251, 'hits': [{'document': {'urn': u}} for u in urns]}
|
|
client_for(monkeypatch, search)
|
|
response = TestClient(api.app).get('/api/schools?search=Alpha&local_authority=Testshire')
|
|
assert response.status_code == 200
|
|
assert response.json()['total'] == 1
|
|
assert response.json()['schools'][0]['urn'] == 100001
|
|
|
|
|
|
def test_unavailable_dataset_is_not_a_missing_school_or_empty_search(monkeypatch):
|
|
import pandas as pd
|
|
monkeypatch.setattr(api.limiter, 'enabled', False)
|
|
monkeypatch.setattr(api, 'load_school_data', lambda: pd.DataFrame())
|
|
monkeypatch.setattr(api, 'load_latest_school_data', lambda: pd.DataFrame())
|
|
client = TestClient(api.app)
|
|
assert client.get('/api/schools/100001').status_code == 503
|
|
assert client.get('/api/schools?search=school').status_code == 503
|