forked from ChelseaKR/disclosed
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_sources.py
More file actions
215 lines (182 loc) · 8.8 KB
/
Copy pathtest_sources.py
File metadata and controls
215 lines (182 loc) · 8.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
"""The source adapter, including the failure it must not paper over.
The adapter's one job beyond fetching is to refuse to return partial data. A truncated fetch would
understate disclosure across every institution that never arrived, which on the published page looks
exactly like a real collapse in reporting. These tests exist mostly to hold that line.
"""
from __future__ import annotations
import io
import json
import urllib.error
from collections.abc import Iterator
from typing import Any
import pytest
from disclosed.fields import SCORECARD_API_FIELDS
from disclosed.sources import college_scorecard
class _FakeResponse(io.StringIO):
def __enter__(self) -> _FakeResponse:
return self
def __exit__(self, *exc: object) -> None:
self.close()
def _page(results: list[dict[str, Any]], total: int) -> _FakeResponse:
return _FakeResponse(json.dumps({"metadata": {"total": total}, "results": results}))
@pytest.fixture
def captured_urls(monkeypatch: pytest.MonkeyPatch) -> list[str]:
urls: list[str] = []
def fake_urlopen(url: str, timeout: float = 0) -> _FakeResponse:
urls.append(url)
return _page([{"id": len(urls), "school.name": f"School {len(urls)}"}], total=1)
monkeypatch.setattr(college_scorecard.urllib.request, "urlopen", fake_urlopen)
return urls
class TestFetchPage:
def test_requests_every_graded_field(self, captured_urls: list[str]) -> None:
college_scorecard.fetch_page(0)
(url,) = captured_urls
for field in SCORECARD_API_FIELDS:
assert field in url
def test_uses_demo_key_by_default(
self, captured_urls: list[str], monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.delenv("DATA_GOV_API_KEY", raising=False)
college_scorecard.fetch_page(0)
assert "api_key=DEMO_KEY" in captured_urls[0]
def test_prefers_env_key_when_present(
self, captured_urls: list[str], monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("DATA_GOV_API_KEY", "realkey123")
college_scorecard.fetch_page(0)
assert "api_key=realkey123" in captured_urls[0]
def test_transport_failure_raises_rather_than_returning_empty(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A page we could not read must never look like a page with nothing in it."""
def boom(url: str, timeout: float = 0) -> _FakeResponse:
raise urllib.error.URLError("connection reset")
monkeypatch.setattr(college_scorecard.urllib.request, "urlopen", boom)
with pytest.raises(college_scorecard.ScorecardError, match="unreadable"):
college_scorecard.fetch_page(3)
def test_undecodable_body_raises(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
college_scorecard.urllib.request,
"urlopen",
lambda url, timeout=0: _FakeResponse("not json at all"),
)
with pytest.raises(college_scorecard.ScorecardError):
college_scorecard.fetch_page(0)
def test_non_object_payload_raises(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
college_scorecard.urllib.request,
"urlopen",
lambda url, timeout=0: _FakeResponse("[1, 2, 3]"),
)
with pytest.raises(college_scorecard.ScorecardError, match="non-object"):
college_scorecard.fetch_page(0)
class TestIterInstitutions:
def test_pages_until_total_is_reached(self, monkeypatch: pytest.MonkeyPatch) -> None:
pages = [
_page([{"id": 1}, {"id": 2}], total=3),
_page([{"id": 3}], total=3),
]
it: Iterator[_FakeResponse] = iter(pages)
monkeypatch.setattr(
college_scorecard.urllib.request, "urlopen", lambda url, timeout=0: next(it)
)
assert [r["id"] for r in college_scorecard.iter_institutions()] == [1, 2, 3]
def test_limit_stops_early(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
college_scorecard.urllib.request,
"urlopen",
lambda url, timeout=0: _page([{"id": 1}, {"id": 2}, {"id": 3}], total=99),
)
assert len(list(college_scorecard.iter_institutions(limit=2))) == 2
def test_empty_results_ends_iteration(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
college_scorecard.urllib.request,
"urlopen",
lambda url, timeout=0: _page([], total=0),
)
assert list(college_scorecard.iter_institutions()) == []
def test_non_dict_rows_are_skipped_not_yielded(self, monkeypatch: pytest.MonkeyPatch) -> None:
it = iter([_page([{"id": 1}, "junk", {"id": 2}], total=2)]) # type: ignore[list-item]
monkeypatch.setattr(
college_scorecard.urllib.request, "urlopen", lambda url, timeout=0: next(it)
)
assert [r["id"] for r in college_scorecard.iter_institutions()] == [1, 2]
def test_missing_metadata_raises_rather_than_reporting_a_completed_walk(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A malformed payload must terminate rather than page indefinitely -- but a walk that
cannot show it reached the end is a failure, not a quiet 1-institution success. This is
the second shape from issue #1: a 200 with no metadata at all mid-walk."""
pages = [
_FakeResponse(json.dumps({"results": [{"id": 1}]})),
_FakeResponse(json.dumps({"results": []})),
]
it = iter(pages)
monkeypatch.setattr(
college_scorecard.urllib.request, "urlopen", lambda url, timeout=0: next(it)
)
with pytest.raises(college_scorecard.ScorecardError, match="page 1"):
list(college_scorecard.iter_institutions())
def test_missing_metadata_does_not_loop_forever_when_limited(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The same malformed payload, but the caller asked to stop early: a deliberate --limit
run keeps whatever arrived and returns silently, exactly as before this fix."""
pages = [
_FakeResponse(json.dumps({"results": [{"id": 1}]})),
_FakeResponse(json.dumps({"results": []})),
]
it = iter(pages)
monkeypatch.setattr(
college_scorecard.urllib.request, "urlopen", lambda url, timeout=0: next(it)
)
assert [r["id"] for r in college_scorecard.iter_institutions(limit=5)] == [1]
def test_results_run_out_before_total_raises_when_walking_to_exhaustion(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Issue #1's first reproduction: two good pages, then a well-formed HTTP 200 carrying an
empty ``results`` list well short of ``metadata.total``. A 200 with nothing in it is not
evidence the walk finished, so a national run must fail loudly rather than publish 3 of
6,300 institutions as the whole country."""
pages = [
_page([{"id": 1}, {"id": 2}], total=6300),
_page([{"id": 3}], total=6300),
_page([], total=6300),
]
it = iter(pages)
monkeypatch.setattr(
college_scorecard.urllib.request, "urlopen", lambda url, timeout=0: next(it)
)
with pytest.raises(college_scorecard.ScorecardError, match="page 2") as caught:
list(college_scorecard.iter_institutions())
assert "3" in str(caught.value)
assert "6300" in str(caught.value)
def test_error_payload_mid_walk_raises_when_walking_to_exhaustion(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Issue #1's second reproduction: a 200 carrying ``{"errors": [...]}`` instead of
``results``, partway through a national walk."""
pages = [
_page([{"id": 1}, {"id": 2}], total=6300),
_FakeResponse(json.dumps({"errors": ["rate governor engaged"]})),
]
it = iter(pages)
monkeypatch.setattr(
college_scorecard.urllib.request, "urlopen", lambda url, timeout=0: next(it)
)
with pytest.raises(college_scorecard.ScorecardError, match="page 1"):
list(college_scorecard.iter_institutions())
def test_limit_short_circuits_even_when_results_run_out_first(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A deliberate --limit is a sample by request. Running out of results before reaching it
is not the anomaly this fix guards against, and must keep returning silently."""
pages = [
_page([{"id": 1}, {"id": 2}], total=6300),
_page([], total=6300),
]
it = iter(pages)
monkeypatch.setattr(
college_scorecard.urllib.request, "urlopen", lambda url, timeout=0: next(it)
)
assert [r["id"] for r in college_scorecard.iter_institutions(limit=50)] == [1, 2]