Skip to content

Commit 02ea27b

Browse files
authored
feat(api): support the historic joa api endpoint (#12)
* Add query parameter models to HistoricJoaEndpoint * Add response shape classes to HistoricJoaEndpoint * Normalize date and boolean parameter and response types * Write generator methods for querying Historic JOA by page or by item * Create pytest fixtures for HistoricJoaEndpoint unit tests * Implement HistoricJoaEndpoint unit tests
1 parent d73f092 commit 02ea27b

File tree

7 files changed

+889
-13
lines changed

7 files changed

+889
-13
lines changed

tests/conftest.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
from typing import Dict, List
2+
3+
import pytest
4+
5+
6+
@pytest.fixture
7+
def historicjoa_params_kwargs() -> Dict[str, str]:
8+
"""Field-value mapping used to build HistoricJoaEndpoint params models."""
9+
10+
return {
11+
"hiring_agency_codes": "AGENCY1",
12+
"hiring_department_codes": "DEPT1",
13+
"position_series": "2210",
14+
"announcement_numbers": "23-ABC",
15+
"usajobs_control_numbers": "1234567",
16+
"start_position_open_date": "2020-01-01",
17+
"end_position_open_date": "2020-12-31",
18+
"start_position_close_date": "2021-01-01",
19+
"end_position_close_date": "2021-12-31",
20+
"continuation_token": "token123",
21+
}
22+
23+
24+
@pytest.fixture
25+
def historicjoa_response_payload() -> Dict[str, object]:
26+
"""Serialized Historic JOA response payload mimicking the USAJOBS API."""
27+
28+
return {
29+
"paging": {
30+
"metadata": {
31+
"totalCount": 2,
32+
"pageSize": 2,
33+
"continuationToken": "NEXTTOKEN",
34+
},
35+
"next": "https://example.invalid/historicjoa?page=2",
36+
},
37+
"data": _historicjoa_items(),
38+
}
39+
40+
41+
def _historicjoa_items() -> List[Dict[str, object]]:
42+
return [
43+
{
44+
"usajobsControlNumber": 123456789,
45+
"hiringAgencyCode": "NASA",
46+
"hiringAgencyName": "National Aeronautics and Space Administration",
47+
"hiringDepartmentCode": "NAT",
48+
"hiringDepartmentName": "Department of Science",
49+
"agencyLevel": 2,
50+
"agencyLevelSort": "Department of Science\\NASA",
51+
"appointmentType": "Permanent",
52+
"workSchedule": "Full-time",
53+
"payScale": "GS",
54+
"salaryType": "Per Year",
55+
"vendor": "USASTAFFING",
56+
"travelRequirement": "Occasional travel",
57+
"teleworkEligible": "Y",
58+
"serviceType": "Competitive",
59+
"securityClearanceRequired": "Y",
60+
"securityClearance": "Secret",
61+
"whoMayApply": "United States Citizens",
62+
"announcementClosingTypeCode": "C",
63+
"announcementClosingTypeDescription": "Closing Date",
64+
"positionOpenDate": "2020-01-01",
65+
"positionCloseDate": "2020-02-01",
66+
"positionExpireDate": None,
67+
"announcementNumber": "NASA-20-001",
68+
"hiringSubelementName": "Space Operations",
69+
"positionTitle": "Data Scientist",
70+
"minimumGrade": "12",
71+
"maximumGrade": "13",
72+
"promotionPotential": "13",
73+
"minimumSalary": 90000.0,
74+
"maximumSalary": 120000.0,
75+
"supervisoryStatus": "N",
76+
"drugTestRequired": "N",
77+
"relocationExpensesReimbursed": "Y",
78+
"totalOpenings": "3",
79+
"disableApplyOnline": "N",
80+
"positionOpeningStatus": "Accepting Applications",
81+
"hiringPaths": [{"hiringPath": "The public"}],
82+
"jobCategories": [{"series": "1550"}],
83+
"positionLocations": [
84+
{
85+
"positionLocationCity": "Houston",
86+
"positionLocationState": "Texas",
87+
"positionLocationCountry": "United States",
88+
}
89+
],
90+
},
91+
{
92+
"usajobsControlNumber": 987654321,
93+
"hiringAgencyCode": "DOE",
94+
"hiringAgencyName": "Department of Energy",
95+
"hiringDepartmentCode": "ENG",
96+
"hiringDepartmentName": "Department of Energy",
97+
"agencyLevel": 1,
98+
"agencyLevelSort": "Department of Energy",
99+
"appointmentType": "Term",
100+
"workSchedule": "Part-time",
101+
"payScale": "GS",
102+
"salaryType": "Per Year",
103+
"vendor": "OTHER",
104+
"travelRequirement": "Not required",
105+
"teleworkEligible": "N",
106+
"serviceType": None,
107+
"securityClearanceRequired": "N",
108+
"securityClearance": "Not Required",
109+
"whoMayApply": "Agency Employees Only",
110+
"announcementClosingTypeCode": None,
111+
"announcementClosingTypeDescription": None,
112+
"positionOpenDate": "2020-03-01",
113+
"positionCloseDate": "2020-04-01",
114+
"positionExpireDate": "2020-04-15",
115+
"announcementNumber": "DOE-20-ENG",
116+
"hiringSubelementName": "Energy Research",
117+
"positionTitle": "Backend Engineer",
118+
"minimumGrade": "11",
119+
"maximumGrade": "12",
120+
"promotionPotential": None,
121+
"minimumSalary": 80000.0,
122+
"maximumSalary": 110000.0,
123+
"supervisoryStatus": "Y",
124+
"drugTestRequired": "Y",
125+
"relocationExpensesReimbursed": "N",
126+
"totalOpenings": "1",
127+
"disableApplyOnline": "Y",
128+
"positionOpeningStatus": "Closed",
129+
"hiringPaths": [{"hiringPath": "Government employees"}],
130+
"jobCategories": [{"series": "2210"}],
131+
"positionLocations": [
132+
{
133+
"positionLocationCity": "Washington",
134+
"positionLocationState": "District of Columbia",
135+
"positionLocationCountry": "United States",
136+
}
137+
],
138+
},
139+
]

tests/unit/test_client.py

Lines changed: 195 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,197 @@
1-
"""Placeholder tests for USAJobs API package."""
1+
"""Unit tests for USAJobsApiClient."""
22

3+
from copy import deepcopy
34

4-
def test_placeholder() -> None:
5-
"""A trivial test to ensure the test suite runs."""
6-
assert True
5+
import pytest
6+
7+
from usajobsapi.client import USAJobsApiClient
8+
from usajobsapi.endpoints.historicjoa import HistoricJoaEndpoint
9+
10+
# test historic_joa_pages
11+
# ---
12+
13+
14+
def test_historic_joa_pages_yields_pages(
15+
monkeypatch, historicjoa_response_payload
16+
) -> None:
17+
"""Ensure historic_joa_pages yields pages while forwarding continuation tokens."""
18+
19+
first_payload = deepcopy(historicjoa_response_payload)
20+
second_payload = deepcopy(historicjoa_response_payload)
21+
second_payload["paging"]["metadata"]["continuationToken"] = None
22+
second_payload["data"] = []
23+
24+
responses = [
25+
HistoricJoaEndpoint.Response.model_validate(first_payload),
26+
HistoricJoaEndpoint.Response.model_validate(second_payload),
27+
]
28+
captured_kwargs = []
29+
30+
def fake_historic_joa(self, **call_kwargs):
31+
captured_kwargs.append(call_kwargs)
32+
return responses.pop(0)
33+
34+
monkeypatch.setattr(USAJobsApiClient, "historic_joa", fake_historic_joa)
35+
36+
client = USAJobsApiClient()
37+
38+
pages = list(
39+
client.historic_joa_pages(
40+
hiring_agency_codes="NASA", continuation_token="INITIALTOKEN"
41+
)
42+
)
43+
44+
assert len(pages) == 2
45+
assert pages[0].next_token() == "NEXTTOKEN"
46+
assert pages[1].next_token() is None
47+
assert captured_kwargs == [
48+
{"hiring_agency_codes": "NASA", "continuation_token": "INITIALTOKEN"},
49+
{"hiring_agency_codes": "NASA", "continuation_token": "NEXTTOKEN"},
50+
]
51+
52+
53+
def test_historic_joa_pages_duplicate_token(
54+
monkeypatch, historicjoa_response_payload
55+
) -> None:
56+
"""Duplicate continuation tokens should raise to avoid infinite loops."""
57+
58+
first_response = HistoricJoaEndpoint.Response.model_validate(
59+
historicjoa_response_payload
60+
)
61+
duplicate_payload = deepcopy(historicjoa_response_payload)
62+
duplicate_payload["paging"]["metadata"]["continuationToken"] = (
63+
first_response.next_token()
64+
)
65+
responses = [
66+
first_response,
67+
HistoricJoaEndpoint.Response.model_validate(duplicate_payload),
68+
]
69+
70+
def fake_historic_joa(self, **_):
71+
return responses.pop(0)
72+
73+
monkeypatch.setattr(USAJobsApiClient, "historic_joa", fake_historic_joa)
74+
75+
client = USAJobsApiClient()
76+
iterator = client.historic_joa_pages()
77+
78+
assert next(iterator)
79+
with pytest.raises(RuntimeError, match="duplicate continuation token"):
80+
next(iterator)
81+
82+
83+
# test historic_joa_items
84+
# ---
85+
86+
87+
def test_historic_joa_items_yields_items_across_pages(
88+
monkeypatch: pytest.MonkeyPatch, historicjoa_response_payload
89+
) -> None:
90+
"""Ensure historic_joa_items yields items and follows continuation tokens."""
91+
92+
client = USAJobsApiClient()
93+
94+
first_page = deepcopy(historicjoa_response_payload)
95+
first_page["paging"]["metadata"]["continuationToken"] = "TOKEN2"
96+
first_page["data"] = first_page["data"][:2]
97+
98+
second_page = {
99+
"paging": {
100+
"metadata": {"totalCount": 3, "pageSize": 1, "continuationToken": None},
101+
"next": None,
102+
},
103+
"data": [
104+
{
105+
"usajobsControlNumber": 111222333,
106+
"hiringAgencyCode": "GSA",
107+
"hiringAgencyName": "General Services Administration",
108+
"hiringDepartmentCode": "GSA",
109+
"hiringDepartmentName": "General Services Administration",
110+
"agencyLevel": 1,
111+
"agencyLevelSort": "GSA",
112+
"appointmentType": "Permanent",
113+
"workSchedule": "Full-time",
114+
"payScale": "GS",
115+
"salaryType": "Per Year",
116+
"vendor": "USASTAFFING",
117+
"travelRequirement": "Not required",
118+
"teleworkEligible": "Y",
119+
"serviceType": "Competitive",
120+
"securityClearanceRequired": "N",
121+
"securityClearance": "Not Required",
122+
"whoMayApply": "All",
123+
"announcementClosingTypeCode": "C",
124+
"announcementClosingTypeDescription": "Closing Date",
125+
"positionOpenDate": "2020-05-01",
126+
"positionCloseDate": "2020-05-15",
127+
"positionExpireDate": None,
128+
"announcementNumber": "GSA-20-001",
129+
"hiringSubelementName": "Administration",
130+
"positionTitle": "Systems Analyst",
131+
"minimumGrade": "11",
132+
"maximumGrade": "12",
133+
"promotionPotential": "13",
134+
"minimumSalary": 85000.0,
135+
"maximumSalary": 95000.0,
136+
"supervisoryStatus": "N",
137+
"drugTestRequired": "N",
138+
"relocationExpensesReimbursed": "N",
139+
"totalOpenings": "2",
140+
"disableApplyOnline": "N",
141+
"positionOpeningStatus": "Accepting Applications",
142+
"hiringPaths": [{"hiringPath": "The public"}],
143+
"jobCategories": [{"series": "2210"}],
144+
"positionLocations": [
145+
{
146+
"positionLocationCity": "Washington",
147+
"positionLocationState": "District of Columbia",
148+
"positionLocationCountry": "United States",
149+
}
150+
],
151+
}
152+
],
153+
}
154+
155+
responses = [first_page, second_page]
156+
calls = []
157+
158+
def fake_historic(**call_kwargs):
159+
calls.append(call_kwargs)
160+
return HistoricJoaEndpoint.Response.model_validate(responses.pop(0))
161+
162+
monkeypatch.setattr(client, "historic_joa", fake_historic)
163+
164+
items = list(client.historic_joa_items(hiring_agency_codes="NASA"))
165+
166+
assert [item.usajobs_control_number for item in items] == [
167+
123456789,
168+
987654321,
169+
111222333,
170+
]
171+
assert len(calls) == 2
172+
assert "continuation_token" not in calls[0]
173+
assert calls[1]["continuation_token"] == "TOKEN2"
174+
175+
176+
def test_historic_joa_items_respects_initial_token(
177+
monkeypatch: pytest.MonkeyPatch, historicjoa_response_payload
178+
) -> None:
179+
"""Ensure historic_joa_items uses the supplied initial continuation token."""
180+
181+
client = USAJobsApiClient()
182+
183+
payload = deepcopy(historicjoa_response_payload)
184+
payload["paging"]["metadata"]["continuationToken"] = None
185+
186+
calls = []
187+
188+
def fake_historic(**call_kwargs):
189+
calls.append(call_kwargs)
190+
return HistoricJoaEndpoint.Response.model_validate(payload)
191+
192+
monkeypatch.setattr(client, "historic_joa", fake_historic)
193+
194+
items = list(client.historic_joa_items(continuation_token="SEED"))
195+
196+
assert len(items) == len(payload["data"])
197+
assert calls[0]["continuation_token"] == "SEED"

0 commit comments

Comments
 (0)