98 lines
3.6 KiB
Python
98 lines
3.6 KiB
Python
"""Tests for the GIAS-driven has_sixth_form flag (spec 2026-07-07 §3).
|
|||
|
|
|
||
|
|
The filter and payloads must use dim_school.has_sixth_form, not the old
|
||
|
|
age_range-contains-"18" substring heuristic. The key regression case is a
|
||
|
|
16-19 sixth-form college: flag true, but "16-19" contains no "18".
|
||
|
|
"""
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
import pandas as pd
|
||
|
|
import pytest
|
||
|
|
from fastapi.testclient import TestClient
|
||
|
|
|
||
|
|
|
||
|
|
def _schools_df() -> pd.DataFrame:
|
||
|
|
"""Latest-year snapshot rows as produced by load_latest_school_data."""
|
||
|
|
base = {
|
||
|
|
"local_authority": "Testshire",
|
||
|
|
"school_type": "Academy",
|
||
|
|
"phase": "Secondary",
|
||
|
|
"address": "1 Test Street",
|
||
|
|
"town": "Testtown",
|
||
|
|
"postcode": "TS1 1AA",
|
||
|
|
"religious_denomination": None,
|
||
|
|
"gender": "Mixed",
|
||
|
|
"admissions_policy": None,
|
||
|
|
"ofsted_grade": np.nan,
|
||
|
|
"ofsted_date": None,
|
||
|
|
"ofsted_framework": None,
|
||
|
|
"latitude": 51.5,
|
||
|
|
"longitude": -0.1,
|
||
|
|
"year": 202425,
|
||
|
|
"total_pupils": 1000,
|
||
|
|
"rwm_expected_pct": np.nan,
|
||
|
|
"attainment_8_score": 50.0,
|
||
|
|
}
|
||
|
|
return pd.DataFrame(
|
||
|
|
[
|
||
|
|
# 11-18 school WITH a registered sixth form
|
||
|
|
{**base, "urn": 100001, "school_name": "Alpha High",
|
||
|
|
"age_range": "11-18", "has_sixth_form": True},
|
||
|
|
# 16-19 college: old heuristic said NO ("16-19" has no "18"),
|
||
|
|
# GIAS flag says YES — must appear in the yes-filter results
|
||
|
|
{**base, "urn": 100002, "school_name": "Beta Sixth Form College",
|
||
|
|
"age_range": "16-19", "has_sixth_form": True},
|
||
|
|
# 11-18 age range on paper but NO registered sixth form:
|
||
|
|
# old heuristic said YES, GIAS flag says NO
|
||
|
|
{**base, "urn": 100003, "school_name": "Gamma Academy",
|
||
|
|
"age_range": "11-18", "has_sixth_form": False},
|
||
|
|
# Missing flag (pipeline not yet re-run) — must not crash,
|
||
|
|
# must not match the yes-filter
|
||
|
|
{**base, "urn": 100004, "school_name": "Delta School",
|
||
|
|
"age_range": "11-16", "has_sixth_form": None},
|
||
|
|
]
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture()
|
||
|
|
def client(monkeypatch):
|
||
|
|
from backend import app as app_module
|
||
|
|
|
||
|
|
monkeypatch.setattr(app_module, "load_latest_school_data", _schools_df)
|
||
|
|
monkeypatch.setattr(app_module, "load_school_data", _schools_df)
|
||
|
|
monkeypatch.setattr(app_module, "get_supplementary_data", lambda db, urn: {})
|
||
|
|
return TestClient(app_module.app, raise_server_exceptions=False)
|
||
|
|
|
||
|
|
|
||
|
|
def _urns(resp):
|
||
|
|
return sorted(s["urn"] for s in resp.json()["schools"])
|
||
|
|
|
||
|
|
|
||
|
|
def test_filter_yes_uses_flag_not_age_range(client):
|
||
|
|
resp = client.get("/api/schools?has_sixth_form=yes")
|
||
|
|
assert resp.status_code == 200, resp.text
|
||
|
|
# 16-19 college included; 11-18-without-sixth-form excluded
|
||
|
|
assert _urns(resp) == [100001, 100002]
|
||
|
|
|
||
|
|
|
||
|
|
def test_filter_no_uses_flag_not_age_range(client):
|
||
|
|
resp = client.get("/api/schools?has_sixth_form=no")
|
||
|
|
assert resp.status_code == 200, resp.text
|
||
|
|
# Gamma (flag false) and Delta (flag missing => not true)
|
||
|
|
assert _urns(resp) == [100003, 100004]
|
||
|
|
|
||
|
|
|
||
|
|
def test_list_payload_includes_flag(client):
|
||
|
|
resp = client.get("/api/schools")
|
||
|
|
assert resp.status_code == 200, resp.text
|
||
|
|
by_urn = {s["urn"]: s for s in resp.json()["schools"]}
|
||
|
|
assert by_urn[100002]["has_sixth_form"] is True
|
||
|
|
assert by_urn[100003]["has_sixth_form"] is False
|
||
|
|
assert by_urn[100004]["has_sixth_form"] is None
|
||
|
|
|
||
|
|
|
||
|
|
def test_detail_payload_includes_flag(client):
|
||
|
|
resp = client.get("/api/schools/100002")
|
||
|
|
assert resp.status_code == 200, resp.text
|
||
|
|
assert resp.json()["school_info"]["has_sixth_form"] is True
|