""" End-to-end tests of the search endpoint against the PostgreSQL backend. They pin down the behaviour the task description asks for: keyword search plus at least two filters, and a correct contract between frontend and backend. """ from __future__ import annotations import pytest from django.urls import reverse pytestmark = pytest.mark.django_db def search(client, **params): # noqa: ANN001, ANN201 return client.get(reverse("v1:search:search"), params) def names(response) -> list[str]: # noqa: ANN001 return [hit["full_name"] for hit in response.data["results"]] def test_empty_query_returns_every_profile( auth_client, analyst, sample_profiles ) -> None: # noqa: ANN001 response = search(auth_client(analyst)) assert response.status_code == 200 assert response.data["count"] == len(sample_profiles) assert response.data["backend"] == "postgres" def test_keyword_matches_skills_and_summary( auth_client, analyst, sample_profiles ) -> None: # noqa: ANN001 response = search(auth_client(analyst), q="django") assert names(response) == ["sara python"] def test_keyword_matches_a_person_name( auth_client, analyst, sample_profiles ) -> None: # noqa: ANN001 assert names(search(auth_client(analyst), q="maria")) == ["maria frontend"] def test_keyword_tolerates_a_typo_in_the_name( auth_client, analyst, sample_profiles ) -> None: # noqa: ANN001 """Trigram similarity is what makes ``jhon`` still find ``john``.""" assert "john recruiter" in names(search(auth_client(analyst), q="jhon recruiter")) def test_skill_filter(auth_client, analyst, sample_profiles) -> None: # noqa: ANN001 assert names(search(auth_client(analyst), skills="react")) == ["maria frontend"] def test_multiple_values_inside_one_filter_are_or_combined( auth_client, analyst, sample_profiles ) -> None: # noqa: ANN001 response = search(auth_client(analyst), skills="react,django") assert sorted(names(response)) == ["maria frontend", "sara python"] def test_different_filters_are_and_combined( auth_client, analyst, sample_profiles ) -> None: # noqa: ANN001 response = search(auth_client(analyst), skills="react,django", countries="spain") assert names(response) == ["maria frontend"] def test_role_and_experience_filters(auth_client, analyst, sample_profiles) -> None: # noqa: ANN001 response = search(auth_client(analyst), roles="engineering", min_experience=5) assert names(response) == ["sara python"] def test_repeated_query_parameters_are_supported( auth_client, analyst, sample_profiles ) -> None: # noqa: ANN001 client = auth_client(analyst) response = client.get(f"{reverse('v1:search:search')}?skills=react&skills=django") assert sorted(names(response)) == ["maria frontend", "sara python"] def test_facets_reflect_the_filtered_result_set( auth_client, analyst, sample_profiles ) -> None: # noqa: ANN001 response = search(auth_client(analyst), roles="engineering") countries = {bucket["key"] for bucket in response.data["facets"]["country"]} assert countries == {"germany", "spain"} assert response.data["facets"]["skills"] def test_selecting_a_facet_value_keeps_its_own_sibling_values_visible( auth_client, analyst, sample_profiles ) -> None: # noqa: ANN001 """ Multi-select facets: ticking "Engineering" under Job family must not make "Human resources" disappear from that same checkbox list - a facet is scoped by every *other* active filter, never by its own. Every other facet (country, in ``test_facets_reflect_the_filtered_result_set`` above) keeps narrowing to the selection as before. """ response = search(auth_client(analyst), roles="engineering") assert sorted(names(response)) == ["maria frontend", "sara python"] roles = {bucket["key"]: bucket["count"] for bucket in response.data["facets"]["job_title_role"]} assert roles == {"engineering": 2, "human_resources": 1} def test_self_exclusion_does_not_ignore_other_active_filters( auth_client, analyst, sample_profiles ) -> None: # noqa: ANN001 """ Excluding a facet's own filter must not turn into excluding *all* filters: with ``roles=engineering`` AND ``countries=germany`` both active, the job-family facet drops only its own ``roles`` filter and keeps ``countries=germany``. "human_resources" (john, based in the United States) correctly stays absent here - not because the facet is narrowed by its own selection, but because no German profile has that role at all. Contrast with ``test_selecting_a_facet_value_keeps_its_own_sibling_values_visible``, where the same "human_resources" sibling *does* reappear once ``countries`` is not also constraining the result set. """ response = search(auth_client(analyst), roles="engineering", countries="germany") assert names(response) == ["sara python"] roles = {bucket["key"] for bucket in response.data["facets"]["job_title_role"]} assert roles == {"engineering"} countries = {bucket["key"] for bucket in response.data["facets"]["country"]} # Scoped by roles=engineering (the other active filter) but not by its # own countries=germany selection, so spain (maria, also engineering) # still shows up. assert countries == {"germany", "spain"} def test_sorting_by_experience(auth_client, analyst, sample_profiles) -> None: # noqa: ANN001 assert names(search(auth_client(analyst), sort="-experience"))[0] == "john recruiter" assert names(search(auth_client(analyst), sort="experience"))[0] == "maria frontend" assert names(search(auth_client(analyst), sort="name"))[0] == "john recruiter" def test_pagination_metadata(auth_client, analyst, sample_profiles) -> None: # noqa: ANN001 response = search(auth_client(analyst), page_size=2, page=2, sort="name") assert response.data["page"] == 2 assert response.data["page_size"] == 2 assert response.data["total_pages"] == 2 assert len(response.data["results"]) == 1 def test_highlight_is_returned_for_keyword_searches( auth_client, analyst, sample_profiles ) -> None: # noqa: ANN001 hit = search(auth_client(analyst), q="django").data["results"][0] assert "" in " ".join(hit["highlight"].get("skills", [])) def test_invalid_experience_range_is_rejected(auth_client, analyst) -> None: # noqa: ANN001 response = search(auth_client(analyst), min_experience=10, max_experience=2) assert response.status_code == 400 assert response.data["error"]["code"] == "validation_error" def test_page_size_is_capped(auth_client, analyst) -> None: # noqa: ANN001 assert search(auth_client(analyst), page_size=5000).status_code == 400 def test_deep_pagination_is_refused(auth_client, analyst) -> None: # noqa: ANN001 assert search(auth_client(analyst), page=99999, page_size=100).status_code == 400 def test_unknown_filter_values_return_an_empty_page_not_an_error( auth_client, analyst, sample_profiles ) -> None: # noqa: ANN001 response = search(auth_client(analyst), skills="cobol") assert response.status_code == 200 assert response.data["count"] == 0 def test_suggest_endpoint(auth_client, analyst, sample_profiles) -> None: # noqa: ANN001 response = auth_client(analyst).get(reverse("v1:search:suggest"), {"q": "mar"}) assert response.status_code == 200 assert "maria frontend" in response.data["suggestions"] def test_skill_dictionary_endpoint(auth_client, analyst, sample_profiles) -> None: # noqa: ANN001 response = auth_client(analyst).get(reverse("v1:profiles:skills"), {"q": "py"}) assert response.status_code == 200 assert any(item["name"] == "python" for item in response.data)