Data Engineer F/H
+-
+
- CLOUD TEMPLE +
- 1 CDI +
- Puteaux - 92 +
Publiée le 20/04/2026 Actualisée le 02/06/2026
+Descriptif du poste
+N/A
+diff --git a/src/job_research/apec/adapter.py b/src/job_research/apec/adapter.py index cae593a..2dbda7c 100644 --- a/src/job_research/apec/adapter.py +++ b/src/job_research/apec/adapter.py @@ -300,4 +300,7 @@ class ApecAdapter: polling=1000, timeout=15_000, ) + final_url = page.url + if not _is_public_apec_detail_url(final_url): + raise ValueError(f"ApecAdapter landed on an unexpected URL after redirects: {final_url}") return page.content() diff --git a/src/job_research/apec/normalize.py b/src/job_research/apec/normalize.py index 5670ec4..78ac275 100644 --- a/src/job_research/apec/normalize.py +++ b/src/job_research/apec/normalize.py @@ -235,6 +235,9 @@ def normalize_apec_listing( description_text = _detail_block_text(soup, "Descriptif du poste") if description_text is None: warnings.append(_warning("description_text", "Description missing from Apec listing")) + elif not _has_useful_text(description_text): + warnings.append(_warning("description_text", "Description is empty or placeholder text")) + description_text = None if source_job_id is not None: normalized_source_job_id = source_job_id @@ -283,7 +286,7 @@ def normalize_apec_listing( company=company_text, location=location if _has_useful_text(location) else None, contract_type=contract_type if _has_useful_text(contract_type) else None, - description_text=description_text or None, + description_text=description_text, published_at=published_at_value, refreshed_at=refreshed_at_value, fetched_at=fetched_at, diff --git a/src/job_research/cli.py b/src/job_research/cli.py index 41a61f7..bdeaddf 100644 --- a/src/job_research/cli.py +++ b/src/job_research/cli.py @@ -115,7 +115,11 @@ def build_profile( cv_signals = extract_cv_signals(cv_text) candidate_profile = build_candidate_profile_output(cv_signals, authored_profile) - save_candidate_profile_yaml(out, candidate_profile) + try: + save_candidate_profile_yaml(out, candidate_profile) + except OSError as exc: + typer.echo(f"Unable to write candidate profile to {out}: {exc}", err=True) + raise typer.Exit(code=1) typer.echo(f"candidate profile written to {out}") warning_count = len(candidate_profile.warnings) diff --git a/tests/apec/test_adapter.py b/tests/apec/test_adapter.py index 567e510..870278e 100644 --- a/tests/apec/test_adapter.py +++ b/tests/apec/test_adapter.py @@ -512,6 +512,27 @@ def test_fetch_listing_html_uses_explicit_company_fallback_chain(monkeypatch) -> assert ".details-offer-list" in wait_script +def test_fetch_listing_html_rejects_redirected_non_apec_urls(monkeypatch) -> None: + page = _FakeDetailPage({}, rendered_html="redirected") + + original_goto = page.goto + + def redirecting_goto(url: str, wait_until: str | None = None) -> None: + original_goto(url, wait_until=wait_until) + page.url = "https://www.apec.fr/candidat/recherche-emploi.html/emploi/recherche-avancee?error=true" + + monkeypatch.setattr(page, "goto", redirecting_goto) + monkeypatch.setattr(adapter_module, "_open_public_page", lambda: _fake_open_public_page(page)) + + with pytest.raises(ValueError, match="unexpected URL after redirects"): + ApecAdapter().fetch_listing_html( + "https://www.apec.fr/candidat/recherche-emploi.html/emploi/detail-offre/111" + ) + + assert len(page.waited_functions) == 1 + assert page.goto_urls == ["https://www.apec.fr/candidat/recherche-emploi.html/emploi/detail-offre/111"] + + def test_fetch_listing_html_rejects_non_apec_hosts() -> None: adapter = ApecAdapter() @@ -524,12 +545,14 @@ def test_fetch_listing_html_reuses_browser_context_across_calls(monkeypatch) -> def __init__(self) -> None: self.goto_urls: list[str] = [] self.default_timeout: int | None = None + self.url = "" def set_default_timeout(self, timeout: int) -> None: self.default_timeout = timeout def goto(self, url: str, wait_until: str | None = None) -> None: self.goto_urls.append(url) + self.url = url def wait_for_function(self, function: str, polling: int | None = None, timeout: int | None = None) -> None: return None diff --git a/tests/apec/test_normalize.py b/tests/apec/test_normalize.py index 292f0ff..c474010 100644 --- a/tests/apec/test_normalize.py +++ b/tests/apec/test_normalize.py @@ -250,3 +250,43 @@ def test_normalize_apec_listing_records_warning_for_placeholder_company() -> Non assert listing.company is None assert listing.refreshed_at == "2026-06-02" assert [warning.field for warning in listing.warnings] == ["company"] + + +def test_normalize_apec_listing_records_warning_for_placeholder_description_text() -> None: + html = """ + +
+Publiée le 20/04/2026 Actualisée le 02/06/2026
+N/A
+