| 1 | import unittest |
| 2 | from unittest.mock import patch |
| 3 | |
| 4 | from lib import jobs |
| 5 | |
| 6 | |
| 7 | class JobsSourceTests(unittest.TestCase): |
| 8 | def test_parse_greenhouse_response_preserves_core_fields(self): |
| 9 | payload = { |
| 10 | "jobs": [ |
| 11 | { |
| 12 | "id": 123, |
| 13 | "title": "Enterprise Security Engineer", |
| 14 | "updated_at": "2026-06-01T10:00:00-05:00", |
| 15 | "absolute_url": "https://boards.greenhouse.io/acme/jobs/123", |
| 16 | "content": "<p>Build SSO and SOC 2 workflows.</p>", |
| 17 | "location": {"name": "Remote"}, |
| 18 | "departments": [{"name": "Engineering"}], |
| 19 | "offices": [{"name": "US"}], |
| 20 | } |
| 21 | ] |
| 22 | } |
| 23 | parsed = jobs.parse_greenhouse_response(payload, board_token="acme") |
| 24 | self.assertEqual(1, len(parsed)) |
| 25 | item = parsed[0] |
| 26 | self.assertEqual("Enterprise Security Engineer", item["title"]) |
| 27 | self.assertEqual("2026-06-01", item["date"]) |
| 28 | self.assertEqual("Engineering", item["department"]) |
| 29 | self.assertIn("SSO", item["description"]) |
| 30 | self.assertEqual("greenhouse", item["provider"]) |
| 31 | |
| 32 | @patch("lib.jobs.http.get_text", return_value=None) |
| 33 | @patch("lib.jobs.http.get") |
| 34 | def test_all_ats_miss_degrades_to_empty(self, mock_get, _mock_text): |
| 35 | # No careers page (get_text None), no web backend, every ATS probe 404s. |
| 36 | mock_get.side_effect = jobs.http.HTTPError("missing", status_code=404) |
| 37 | items, artifact = jobs.search_jobs( |
| 38 | "MissingCo", |
| 39 | ("2026-05-16", "2026-06-16"), |
| 40 | {}, |
| 41 | web_backend="none", |
| 42 | ) |
| 43 | self.assertEqual([], items) |
| 44 | self.assertEqual(0, artifact["resultCount"]) |
| 45 | self.assertEqual("web", artifact["tier"]) |
| 46 | |
| 47 | def test_detect_ats_reads_provider_and_slug_from_embed(self): |
| 48 | html = '<a href="https://jobs.ashbyhq.com/listenlabs/4b17">Open roles</a>' |
| 49 | provider, slug = jobs.detect_ats(html) |
| 50 | self.assertEqual("ashby", provider) |
| 51 | self.assertEqual("listenlabs", slug) |
| 52 | |
| 53 | def test_detect_ats_handles_greenhouse_embed_query(self): |
| 54 | html = '<script src="https://boards.greenhouse.io/embed/job_board?for=acmeco"></script>' |
| 55 | provider, slug = jobs.detect_ats(html) |
| 56 | self.assertEqual("greenhouse", provider) |
| 57 | self.assertEqual("acmeco", slug) |
| 58 | |
| 59 | def test_detect_ats_returns_none_when_no_ats(self): |
| 60 | self.assertEqual((None, None), jobs.detect_ats("<html><body>About us</body></html>")) |
| 61 | |
| 62 | def test_parse_ashby_response_core_fields(self): |
| 63 | payload = { |
| 64 | "jobs": [ |
| 65 | { |
| 66 | "id": "abc", |
| 67 | "title": "Founding Research Scientist, Human Simulation", |
| 68 | "departmentName": "Research", |
| 69 | "locationName": "San Francisco", |
| 70 | "publishedDate": "2026-06-10", |
| 71 | "jobUrl": "https://jobs.ashbyhq.com/listenlabs/abc", |
| 72 | "descriptionPlain": "Build human simulation models.", |
| 73 | } |
| 74 | ] |
| 75 | } |
| 76 | parsed = jobs.parse_ashby_response(payload, slug="listenlabs") |
| 77 | self.assertEqual(1, len(parsed)) |
| 78 | item = parsed[0] |
| 79 | self.assertEqual("Founding Research Scientist, Human Simulation", item["title"]) |
| 80 | self.assertEqual("Research", item["department"]) |
| 81 | self.assertEqual("2026-06-10", item["date"]) |
| 82 | self.assertEqual("ashby", item["provider"]) |
| 83 | |
| 84 | def test_parse_lever_response_handles_list_and_epoch(self): |
| 85 | payload = [ |
| 86 | { |
| 87 | "id": "xyz", |
| 88 | "text": "Staff Engineer", |
| 89 | "hostedUrl": "https://jobs.lever.co/acme/xyz", |
| 90 | "categories": {"department": "Engineering", "location": "Remote"}, |
| 91 | "createdAt": 1749513600000, |
| 92 | "descriptionPlain": "Own the platform.", |
| 93 | } |
| 94 | ] |
| 95 | parsed = jobs.parse_lever_response(payload, slug="acme") |
| 96 | self.assertEqual(1, len(parsed)) |
| 97 | self.assertEqual("Staff Engineer", parsed[0]["title"]) |
| 98 | self.assertEqual("Engineering", parsed[0]["department"]) |
| 99 | self.assertRegex(parsed[0]["date"], r"\d{4}-\d{2}-\d{2}") |
| 100 | self.assertEqual("lever", parsed[0]["provider"]) |
| 101 | |
| 102 | def test_parse_smartrecruiters_response_core_fields(self): |
| 103 | payload = { |
| 104 | "content": [ |
| 105 | { |
| 106 | "id": "777", |
| 107 | "name": "Account Executive", |
| 108 | "department": {"label": "Sales"}, |
| 109 | "location": {"city": "New York", "country": "us"}, |
| 110 | "releasedDate": "2026-06-02T00:00:00.000Z", |
| 111 | } |
| 112 | ] |
| 113 | } |
| 114 | parsed = jobs.parse_smartrecruiters_response(payload, slug="acme") |
| 115 | self.assertEqual(1, len(parsed)) |
| 116 | self.assertEqual("Account Executive", parsed[0]["title"]) |
| 117 | self.assertEqual("Sales", parsed[0]["department"]) |
| 118 | self.assertEqual("2026-06-02", parsed[0]["date"]) |
| 119 | self.assertEqual("smartrecruiters", parsed[0]["provider"]) |
| 120 | |
| 121 | def test_extract_jsonld_jobs_parses_jobposting(self): |
| 122 | html = ( |
| 123 | '<script type="application/ld+json">' |
| 124 | '{"@context":"https://schema.org","@type":"JobPosting",' |
| 125 | '"title":"Product Designer","datePosted":"2026-06-05",' |
| 126 | '"description":"<p>Design things.</p>",' |
| 127 | '"url":"https://acme.com/jobs/1",' |
| 128 | '"jobLocation":{"@type":"Place","address":{"@type":"PostalAddress",' |
| 129 | '"addressLocality":"SF","addressCountry":"US"}}}' |
| 130 | '</script>' |
| 131 | ) |
| 132 | parsed = jobs.extract_jsonld_jobs(html, "https://acme.com/careers") |
| 133 | self.assertEqual(1, len(parsed)) |
| 134 | self.assertEqual("Product Designer", parsed[0]["title"]) |
| 135 | self.assertEqual("2026-06-05", parsed[0]["date"]) |
| 136 | self.assertEqual("careers-jsonld", parsed[0]["provider"]) |
| 137 | self.assertIn("SF", parsed[0]["location"]) |
| 138 | |
| 139 | def test_extract_jsonld_jobs_handles_graph_arrays(self): |
| 140 | html = ( |
| 141 | '<script type="application/ld+json">' |
| 142 | '{"@graph":[{"@type":"Organization","name":"Acme"},' |
| 143 | '{"@type":"JobPosting","title":"GTM Lead","datePosted":"2026-06-01"}]}' |
| 144 | '</script>' |
| 145 | ) |
| 146 | parsed = jobs.extract_jsonld_jobs(html, "https://acme.com/careers") |
| 147 | self.assertEqual(1, len(parsed)) |
| 148 | self.assertEqual("GTM Lead", parsed[0]["title"]) |
| 149 | |
| 150 | def test_jsonld_jobs_without_urls_survive_normalize_and_dedupe(self): |
| 151 | from lib import dedupe, normalize |
| 152 | |
| 153 | html = ( |
| 154 | '<script type="application/ld+json">' |
| 155 | '{"@graph":[' |
| 156 | '{"@type":"JobPosting","title":"Founding Designer","datePosted":"2026-06-01"},' |
| 157 | '{"@type":"JobPosting","title":"GTM Lead","datePosted":"2026-06-02"},' |
| 158 | '{"@type":"JobPosting","title":"Staff Engineer","datePosted":"2026-06-03"}' |
| 159 | ']}' |
| 160 | '</script>' |
| 161 | ) |
| 162 | parsed = jobs.extract_jsonld_jobs(html, "https://acme.com/careers") |
| 163 | self.assertEqual(3, len(parsed)) |
| 164 | self.assertTrue(all(item["url"] == "" for item in parsed)) |
| 165 | self.assertTrue(all(item["source_url"] == "https://acme.com/careers" for item in parsed)) |
| 166 | |
| 167 | normalized = normalize.normalize_source_items("jobs", parsed, "2026-05-17", "2026-06-16") |
| 168 | kept = dedupe.dedupe_items(normalized) |
| 169 | self.assertEqual( |
| 170 | ["Founding Designer", "GTM Lead", "Staff Engineer"], |
| 171 | sorted(item.title for item in kept), |
| 172 | ) |
| 173 | |
| 174 | |
| 175 | if __name__ == "__main__": |
| 176 | unittest.main() |
| 177 | |
| 178 | |
| 179 | class JobsDateAndBannerTests(unittest.TestCase): |
| 180 | def test_jobs_keep_open_roles_outside_date_window(self): |
| 181 | from lib import normalize |
| 182 | raw = [ |
| 183 | {"id": "1", "title": "Founding Research Scientist", "url": "https://x/1", |
| 184 | "date": "2026-01-10", "provider": "ashby"}, # posted months ago, still open |
| 185 | {"id": "2", "title": "Account Executive", "url": "https://x/2", |
| 186 | "date": "2026-06-10", "provider": "ashby"}, # within window |
| 187 | ] |
| 188 | items = normalize.normalize_source_items("jobs", raw, "2026-05-17", "2026-06-16") |
| 189 | titles = [i.title for i in items] |
| 190 | self.assertIn("Founding Research Scientist", titles) # not dropped by date window |
| 191 | self.assertEqual(2, len(items)) |
| 192 | |
| 193 | |
| 194 | class JobsDedupeTests(unittest.TestCase): |
| 195 | def test_jobs_dedupe_by_url_not_fuzzy_boilerplate(self): |
| 196 | from lib import dedupe, schema |
| 197 | # Distinct roles sharing heavy boilerplate but with unique URLs. |
| 198 | boiler = "TL;DR: Listen Labs is a fast-growing early-stage startup. Roll up your sleeves." |
| 199 | items = [ |
| 200 | schema.SourceItem(item_id=f"AB{i}", source="jobs", title=t, body=boiler, |
| 201 | url=f"https://jobs.ashbyhq.com/listenlabs/{i}") |
| 202 | for i, t in enumerate(["Founding Research Scientist", "Account Executive", |
| 203 | "Growth Associate", "Business Development Rep"]) |
| 204 | ] |
| 205 | kept = dedupe.dedupe_items(items) |
| 206 | self.assertEqual(4, len(kept)) # all distinct URLs survive |
| 207 | |
| 208 | def test_jobs_dedupe_collapses_same_url(self): |
| 209 | from lib import dedupe, schema |
| 210 | items = [ |
| 211 | schema.SourceItem(item_id="AB1", source="jobs", title="Role", body="x", |
| 212 | url="https://jobs.ashbyhq.com/listenlabs/1"), |
| 213 | schema.SourceItem(item_id="AB1b", source="jobs", title="Role", body="x", |
| 214 | url="https://jobs.ashbyhq.com/listenlabs/1"), |
| 215 | ] |
| 216 | self.assertEqual(1, len(dedupe.dedupe_items(items))) |
| 217 |