You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
299 lines
8.7 KiB
Python
299 lines
8.7 KiB
Python
"""Tests for ATS board crawling (mock HTTP — no network)."""
|
|
from gimme_job.proactive.boards import (
|
|
BoardConfig,
|
|
_parse_icims_jobs,
|
|
fetch_board,
|
|
fetch_greenhouse,
|
|
fetch_lever,
|
|
fetch_smartrecruiters,
|
|
fetch_usajobs,
|
|
fetch_workday,
|
|
)
|
|
|
|
|
|
class _FakeClient:
|
|
"""Stand-in for httpx.Client: returns a configured response for each call."""
|
|
|
|
def __init__(self, responses):
|
|
self._responses = list(responses)
|
|
self._calls = 0
|
|
self.bodies = []
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *a):
|
|
return False
|
|
|
|
def _next(self):
|
|
self._calls += 1
|
|
return self._responses[self._calls - 1]
|
|
|
|
def get(self, url):
|
|
return self._next()
|
|
|
|
def post(self, url, json=None):
|
|
self.bodies.append(json or {})
|
|
return self._next()
|
|
|
|
|
|
def _patch(monkeypatch, client):
|
|
monkeypatch.setattr("httpx.Client", lambda *a, **k: client)
|
|
|
|
|
|
class _Resp:
|
|
def __init__(self, **kw):
|
|
self._kw = kw
|
|
|
|
def raise_for_status(self):
|
|
pass
|
|
|
|
def json(self):
|
|
return self._kw.get("json", {})
|
|
|
|
@property
|
|
def content(self):
|
|
return self._kw.get("content", b"")
|
|
|
|
|
|
def test_greenhouse_filters_dental(monkeypatch):
|
|
client = _FakeClient(
|
|
[
|
|
_Resp(
|
|
json={
|
|
"jobs": [
|
|
{
|
|
"title": "Orthodontist",
|
|
"absolute_url": "https://gh/j/1",
|
|
"content": "braces",
|
|
},
|
|
{
|
|
"title": "Office Manager",
|
|
"absolute_url": "https://gh/j/2",
|
|
"content": "admin",
|
|
},
|
|
]
|
|
}
|
|
)
|
|
]
|
|
)
|
|
_patch(monkeypatch, client)
|
|
res = fetch_greenhouse(BoardConfig(name="Test", org="testorg", source="greenhouse"))
|
|
assert len(res) == 1
|
|
assert res[0].title == "Orthodontist"
|
|
assert res[0].url == "https://gh/j/1"
|
|
assert res[0].source == "greenhouse"
|
|
|
|
|
|
def test_greenhouse_ignores_employer_name_in_match(monkeypatch):
|
|
payload = {
|
|
"jobs": [
|
|
{
|
|
"title": "Account Payable Specialist at Specialty Dental Brands",
|
|
"absolute_url": "https://gh/j/1",
|
|
"content": "finance role",
|
|
},
|
|
{
|
|
"title": "Dentist at Anthem Pediatric Dentistry",
|
|
"absolute_url": "https://gh/j/2",
|
|
"content": "braces support",
|
|
},
|
|
]
|
|
}
|
|
client = _FakeClient([_Resp(json=payload)])
|
|
_patch(monkeypatch, client)
|
|
res = fetch_greenhouse(
|
|
BoardConfig(name="Specialty Dental Brands", org="sdb", source="greenhouse")
|
|
)
|
|
assert len(res) == 1
|
|
assert res[0].title.startswith("Dentist")
|
|
|
|
|
|
def test_boards_role_gate_drops_support_roles(monkeypatch):
|
|
payload = {
|
|
"jobs": [
|
|
{
|
|
"title": "Dental Assistant",
|
|
"absolute_url": "https://gh/j/1",
|
|
"content": "assist the orthodontic team",
|
|
},
|
|
{
|
|
"title": "Orthodontist",
|
|
"absolute_url": "https://gh/j/2",
|
|
"content": "braces",
|
|
},
|
|
]
|
|
}
|
|
client = _FakeClient([_Resp(json=payload)])
|
|
_patch(monkeypatch, client)
|
|
res = fetch_greenhouse(BoardConfig(name="Acme", org="acme", source="greenhouse"))
|
|
assert [r.title for r in res] == ["Orthodontist"]
|
|
|
|
|
|
def test_boards_drop_non_ortho_specialties(monkeypatch):
|
|
payload = {
|
|
"jobs": [
|
|
{
|
|
"title": "Pediatric Dentist",
|
|
"absolute_url": "https://gh/j/1",
|
|
"content": "orthodontics referrals",
|
|
},
|
|
{
|
|
"title": "Orthodontist",
|
|
"absolute_url": "https://gh/j/2",
|
|
"content": "braces",
|
|
},
|
|
]
|
|
}
|
|
client = _FakeClient([_Resp(json=payload)])
|
|
_patch(monkeypatch, client)
|
|
res = fetch_greenhouse(BoardConfig(name="Acme", org="acme", source="greenhouse"))
|
|
assert [r.title for r in res] == ["Orthodontist"]
|
|
|
|
|
|
def test_boards_pass_employer_type(monkeypatch):
|
|
payload = {
|
|
"jobs": [
|
|
{
|
|
"title": "Orthodontist",
|
|
"absolute_url": "https://gh/j/1",
|
|
"content": "braces",
|
|
}
|
|
]
|
|
}
|
|
client = _FakeClient([_Resp(json=payload)])
|
|
_patch(monkeypatch, client)
|
|
res = fetch_greenhouse(
|
|
BoardConfig(
|
|
name="Navajo Nation",
|
|
org="navajo",
|
|
source="greenhouse",
|
|
employer_type="tribal",
|
|
)
|
|
)
|
|
assert res[0].employer_type == "tribal"
|
|
|
|
|
|
def test_lever_filters_dental(monkeypatch):
|
|
client = _FakeClient(
|
|
[
|
|
_Resp(
|
|
json=[
|
|
{
|
|
"text": "Dentist II (Orthodontics)",
|
|
"hostedUrl": "https://lever/p/1",
|
|
"categories": {"location": "Phoenix, AZ"},
|
|
"descriptionPlain": "full-time orthodontics",
|
|
},
|
|
{
|
|
"text": "Nurse",
|
|
"hostedUrl": "https://lever/p/2",
|
|
"categories": {},
|
|
"descriptionPlain": "nursing",
|
|
},
|
|
]
|
|
)
|
|
]
|
|
)
|
|
_patch(monkeypatch, client)
|
|
res = fetch_lever(BoardConfig(name="Test", org="testorg", source="lever"))
|
|
assert len(res) == 1
|
|
assert res[0].title == "Dentist II (Orthodontics)"
|
|
|
|
|
|
def test_smartrecruiters_builds_job_url(monkeypatch):
|
|
payload = {
|
|
"content": [
|
|
{
|
|
"name": "Dentist",
|
|
"id": "12345",
|
|
"location": {"city": "Phoenix"},
|
|
},
|
|
]
|
|
}
|
|
# fetcher queries the API once per keyword (orthodont/dentist/dental)
|
|
client = _FakeClient([_Resp(json=payload)] * 3)
|
|
_patch(monkeypatch, client)
|
|
res = fetch_smartrecruiters(
|
|
BoardConfig(name="NH", org="NATIVEHEALTH", source="smartrecruiters")
|
|
)
|
|
assert len(res) == 1
|
|
assert res[0].url == "https://jobs.smartrecruiters.com/NATIVEHEALTH/12345"
|
|
|
|
|
|
def test_workday_filters(monkeypatch):
|
|
payload = {
|
|
"jobPostings": [
|
|
{
|
|
"title": "Orthodontist",
|
|
"externalPath": "/en-US/org/job/R1",
|
|
"locationsText": "OH",
|
|
},
|
|
{
|
|
"title": "Receptionist",
|
|
"externalPath": "/en-US/org/job/R2",
|
|
"locationsText": "OH",
|
|
},
|
|
]
|
|
}
|
|
# fetcher queries the tenant once per keyword (orthodontist/dentist/dental)
|
|
client = _FakeClient([_Resp(json=payload)] * 3)
|
|
_patch(monkeypatch, client)
|
|
res = fetch_workday(
|
|
BoardConfig(
|
|
name="Test",
|
|
org="org",
|
|
tenant="tenant",
|
|
domain="wd5.myworkdayjobs.com",
|
|
source="workday",
|
|
)
|
|
)
|
|
assert len(res) == 1
|
|
assert "myworkdayjobs.com/en-US/org/job/R1" in res[0].url
|
|
# limit=50 is rejected by some tenants (HTTP 400) — always request 20
|
|
assert client.bodies and all(b["limit"] == 20 for b in client.bodies)
|
|
|
|
|
|
def test_icims_parses_search_html():
|
|
html = (
|
|
'<li class="iCIMS_JobCardItem"><div class="row">'
|
|
'<div class="col-xs-6 header left">'
|
|
'<span class="sr-only field-label">Job Locations</span>'
|
|
"<span >US-IN-New Castle</span></div>"
|
|
'<div class="col-xs-12 title">'
|
|
'<a href="https://careers-x.icims.com/jobs/5374/dentist/job?in_iframe=1" '
|
|
'class="iCIMS_Anchor" title="5374 - Dentist"><h3 >\nDentist</h3></a></div>'
|
|
'<div class="col-xs-12 description">Great braces & aligner work</div>'
|
|
"</div></li>"
|
|
).encode()
|
|
parsed = _parse_icims_jobs(html)
|
|
assert parsed == [
|
|
(
|
|
"Dentist",
|
|
"https://careers-x.icims.com/jobs/5374/dentist/job",
|
|
"US-IN-New Castle Great braces & aligner work",
|
|
)
|
|
]
|
|
|
|
|
|
def test_icims_empty_for_garbage():
|
|
assert _parse_icims_jobs(b"not html at all") == []
|
|
|
|
|
|
def test_usajobs_skips_without_key(monkeypatch):
|
|
import os
|
|
|
|
import dotenv
|
|
|
|
# prevent reload of .env (which now has the key) and force a clean env
|
|
monkeypatch.setattr(dotenv, "load_dotenv", lambda *a, **k: None)
|
|
env = dict(os.environ)
|
|
env.pop("USAJOBS_API_KEY", None)
|
|
env.pop("USAJOBS_API_EMAIL", None)
|
|
monkeypatch.setattr(os, "environ", env)
|
|
res = fetch_usajobs(BoardConfig(name="USAJOBS", source="usajobs"))
|
|
assert res == []
|
|
|
|
|
|
def test_fetch_board_unknown_source():
|
|
assert fetch_board(BoardConfig(name="X", source="unknown")) == [] |