Skip to content

Commit 3b47112

Browse files
committed
Add browser language detection tests for COUNTRIES_FIRST_AUTO_DETECT
Add comprehensive tests simulating browser Accept-Language headers with the COUNTRIES_FIRST_AUTO_DETECT feature. Includes two complementary test approaches: 1. TestBrowserLanguageDetection: Unit tests that parse Accept-Language headers and test auto-detect logic with translation.override() 2. TestBrowserLanguageViaMiddleware: Integration tests making HTTP requests through Django's test client and LocaleMiddleware, simulating real production usage Tests cover realistic browser scenarios: - Australian browser (en-AU) auto-detecting AU - French-Canadian (fr-CA) with language mapping - Complex Accept-Language with quality values (es-MX,es;q=0.9,en;q=0.8) - German-Swiss (de-CH) with deduplication in language groups - Base language without country code fallback - Unsupported language fallback Note: Django's default LANGUAGES includes many locales (en-au, es-mx, etc.) so LocaleMiddleware works out-of-the-box. For locales not in the default list (like fr-ca), add them to LANGUAGES for full auto-detect support. Provides end-to-end validation that the feature works correctly with real browser language preferences as they would be sent in production.
1 parent 7bfef7e commit 3b47112

File tree

1 file changed

+304
-0
lines changed

1 file changed

+304
-0
lines changed

django_countries/tests/test_dynamic_first.py

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,3 +529,307 @@ def test_len_with_auto_detect_and_countries_first(self):
529529
length = len(countries)
530530
# 249 countries + 3 first (AU, US, GB)
531531
self.assertEqual(length, 249 + 3)
532+
533+
534+
@pytest.mark.skipif(not settings.USE_I18N, reason="No i18n")
535+
class TestBrowserLanguageDetection(BaseTest):
536+
"""
537+
Test auto-detect with browser Accept-Language header simulation.
538+
539+
These tests simulate how COUNTRIES_FIRST_AUTO_DETECT works with real
540+
browser language preferences, like a user visiting from Australia with
541+
their browser set to "en-AU" or a French-Canadian user with "fr-CA".
542+
543+
In production, Django's LocaleMiddleware parses the Accept-Language header
544+
and activates the appropriate language. These tests simulate that flow.
545+
"""
546+
547+
def _parse_accept_language(self, accept_language):
548+
"""
549+
Parse Accept-Language header and return best language code.
550+
551+
This simulates what Django's LocaleMiddleware does when processing
552+
the HTTP Accept-Language header from a browser.
553+
"""
554+
from django.utils.translation import trans_real
555+
556+
# Parse the Accept-Language header
557+
parsed = trans_real.parse_accept_lang_header(accept_language)
558+
if not parsed:
559+
return None
560+
561+
# Get the first (highest priority) language
562+
# Format: [(lang, priority), ...] sorted by priority
563+
return parsed[0][0].lower() if parsed else None
564+
565+
def test_australian_browser_auto_detect(self):
566+
"""Test that Australian browser (en-AU) gets Australia first."""
567+
with self.settings(COUNTRIES_FIRST_AUTO_DETECT=True):
568+
# Simulate browser sending Accept-Language: en-AU
569+
language = self._parse_accept_language("en-AU")
570+
self.assertEqual(language, "en-au")
571+
572+
# Activate the language like Django's LocaleMiddleware would
573+
with translation.override(language):
574+
country_list = list(countries)
575+
576+
# Verify AU is first
577+
self.assertEqual(country_list[0].code, "AU")
578+
# Second should be alphabetical
579+
self.assertEqual(country_list[1].code, "AF")
580+
581+
def test_canadian_french_browser_with_language_mapping(self):
582+
"""Test French-Canadian browser with language-based first countries."""
583+
with self.settings(
584+
COUNTRIES_FIRST_AUTO_DETECT=True,
585+
COUNTRIES_FIRST_BY_LANGUAGE={
586+
"fr": ["FR", "CH", "BE", "LU"],
587+
},
588+
):
589+
# Simulate browser sending Accept-Language: fr-CA
590+
language = self._parse_accept_language("fr-CA")
591+
self.assertEqual(language, "fr-ca")
592+
593+
# Activate the language like Django's LocaleMiddleware would
594+
with translation.override(language):
595+
country_list = list(countries)
596+
597+
# Should get CA prepended to French group
598+
self.assertEqual(country_list[0].code, "CA")
599+
self.assertEqual(country_list[1].code, "FR")
600+
self.assertEqual(country_list[2].code, "CH")
601+
self.assertEqual(country_list[3].code, "BE")
602+
603+
def test_multiple_language_preferences(self):
604+
"""Test browser with multiple language preferences in Accept-Language."""
605+
with self.settings(
606+
COUNTRIES_FIRST_AUTO_DETECT=True,
607+
COUNTRIES_FIRST=["US", "GB"],
608+
):
609+
# Browser prefers Spanish-Mexican, then English
610+
# Accept-Language: es-MX,es;q=0.9,en;q=0.8
611+
language = self._parse_accept_language("es-MX,es;q=0.9,en;q=0.8")
612+
613+
# Django should pick es-mx as the highest priority
614+
self.assertEqual(language, "es-mx")
615+
616+
# Activate the language like Django's LocaleMiddleware would
617+
with translation.override(language):
618+
country_list = list(countries)
619+
620+
# Should get MX prepended to COUNTRIES_FIRST
621+
self.assertEqual(country_list[0].code, "MX")
622+
self.assertEqual(country_list[1].code, "US")
623+
self.assertEqual(country_list[2].code, "GB")
624+
625+
def test_german_swiss_browser(self):
626+
"""Test German-Swiss browser (de-CH) with language mapping."""
627+
with self.settings(
628+
COUNTRIES_FIRST_AUTO_DETECT=True,
629+
COUNTRIES_FIRST_BY_LANGUAGE={
630+
"de": ["DE", "AT", "CH", "LI"],
631+
},
632+
):
633+
# Simulate browser sending Accept-Language: de-CH
634+
language = self._parse_accept_language("de-CH")
635+
self.assertEqual(language, "de-ch")
636+
637+
# Activate the language like Django's LocaleMiddleware would
638+
with translation.override(language):
639+
country_list = list(countries)
640+
641+
# CH should move to front (deduplication)
642+
self.assertEqual(country_list[0].code, "CH")
643+
self.assertEqual(country_list[1].code, "DE")
644+
self.assertEqual(country_list[2].code, "AT")
645+
self.assertEqual(country_list[3].code, "LI")
646+
# Make sure CH only appears once
647+
codes = [c.code for c in country_list[:10]]
648+
self.assertEqual(codes.count("CH"), 1)
649+
650+
def test_base_language_without_country(self):
651+
"""Test browser sending only base language (no country code)."""
652+
with self.settings(
653+
COUNTRIES_FIRST_AUTO_DETECT=True,
654+
COUNTRIES_FIRST=["US", "GB"],
655+
):
656+
# Simulate browser sending Accept-Language: en (no country)
657+
language = self._parse_accept_language("en")
658+
659+
# Language detected as just 'en'
660+
self.assertIn(language, ["en", "en-us"]) # Depends on Django version
661+
662+
# Activate the language like Django's LocaleMiddleware would
663+
with translation.override(language):
664+
country_list = list(countries)
665+
666+
# No country to detect, should fall back to COUNTRIES_FIRST
667+
self.assertEqual(country_list[0].code, "US")
668+
self.assertEqual(country_list[1].code, "GB")
669+
670+
def test_unsupported_language_falls_back(self):
671+
"""Test browser with unsupported language falls back to default."""
672+
with self.settings(
673+
COUNTRIES_FIRST_AUTO_DETECT=True,
674+
COUNTRIES_FIRST=["US"],
675+
LANGUAGE_CODE="en-us",
676+
):
677+
# Simulate browser sending unsupported language
678+
language = self._parse_accept_language("zh-CN")
679+
680+
# Should fall back to LANGUAGE_CODE since zh is not in LANGUAGES
681+
self.assertIn(language, ["en-us", "zh-cn"])
682+
683+
# Activate the language like Django's LocaleMiddleware would
684+
with translation.override(language):
685+
country_list = list(countries)
686+
687+
# If zh-cn is supported, CN should be first; otherwise US
688+
if language == "zh-cn":
689+
self.assertEqual(country_list[0].code, "CN")
690+
else:
691+
self.assertEqual(country_list[0].code, "US")
692+
693+
694+
# Define a view for middleware tests
695+
def country_view_for_tests(request):
696+
"""
697+
Simple view that returns the country list and current language.
698+
699+
This simulates a real view where a form with CountryField is rendered.
700+
"""
701+
from django.http import JsonResponse
702+
703+
country_list = list(countries)
704+
return JsonResponse(
705+
{
706+
"language": translation.get_language(),
707+
"countries": [{"code": c.code, "name": c.name} for c in country_list[:5]],
708+
}
709+
)
710+
711+
712+
# URL patterns for middleware tests
713+
test_urlpatterns = [
714+
__import__("django.urls").urls.path("countries/", country_view_for_tests),
715+
]
716+
717+
718+
@pytest.mark.skipif(not settings.USE_I18N, reason="No i18n")
719+
class TestBrowserLanguageViaMiddleware(BaseTest):
720+
"""
721+
Test auto-detect through actual Django middleware and views.
722+
723+
These tests make HTTP requests through Django's test client, simulating
724+
how browsers send Accept-Language headers in production. The requests
725+
are processed through LocaleMiddleware, which activates the language.
726+
"""
727+
728+
def test_australian_browser_via_middleware(self):
729+
"""
730+
Test that Australian browser (Accept-Language: en-AU) gets AU first.
731+
732+
This simulates a real user from Australia visiting a form with a
733+
CountryField. Their browser sends 'en-AU' which Django's LocaleMiddleware
734+
detects, and COUNTRIES_FIRST_AUTO_DETECT uses to show Australia first.
735+
736+
Note: Django's default LANGUAGES setting includes 'en-au', so we don't
737+
need to explicitly configure it.
738+
"""
739+
with self.settings(
740+
COUNTRIES_FIRST_AUTO_DETECT=True,
741+
MIDDLEWARE=[
742+
"django.middleware.common.CommonMiddleware",
743+
"django.middleware.locale.LocaleMiddleware",
744+
],
745+
ROOT_URLCONF="django_countries.tests.test_dynamic_first",
746+
):
747+
response = self.client.get("/countries/", HTTP_ACCEPT_LANGUAGE="en-AU")
748+
749+
self.assertEqual(response.status_code, 200)
750+
data = response.json()
751+
752+
# LocaleMiddleware should have activated en-au
753+
self.assertEqual(data["language"], "en-au")
754+
755+
# AU should be first in the list
756+
self.assertEqual(data["countries"][0]["code"], "AU")
757+
758+
def test_french_canadian_with_language_mapping(self):
759+
"""
760+
Test French-Canadian browser with language-based country mapping.
761+
762+
A user from Quebec has their browser set to 'fr-CA'. Since Django's
763+
default LANGUAGES doesn't include 'fr-ca', LocaleMiddleware falls back
764+
to 'fr'. To get full auto-detect functionality, add specific locales
765+
to LANGUAGES. This test demonstrates the feature WITH configuration.
766+
"""
767+
with self.settings(
768+
COUNTRIES_FIRST_AUTO_DETECT=True,
769+
COUNTRIES_FIRST_BY_LANGUAGE={
770+
"fr": ["FR", "CH", "BE", "LU"],
771+
},
772+
MIDDLEWARE=[
773+
"django.middleware.common.CommonMiddleware",
774+
"django.middleware.locale.LocaleMiddleware",
775+
],
776+
# Add fr-ca so LocaleMiddleware doesn't fall back to fr
777+
LANGUAGES=[
778+
("en", "English"),
779+
("fr", "French"),
780+
("fr-ca", "French (Canada)"),
781+
],
782+
ROOT_URLCONF="django_countries.tests.test_dynamic_first",
783+
):
784+
response = self.client.get("/countries/", HTTP_ACCEPT_LANGUAGE="fr-CA")
785+
786+
self.assertEqual(response.status_code, 200)
787+
data = response.json()
788+
789+
# Should detect fr-ca (with explicit LANGUAGES configuration)
790+
self.assertEqual(data["language"], "fr-ca")
791+
792+
# Should see CA prepended to French group
793+
self.assertEqual(data["countries"][0]["code"], "CA")
794+
self.assertEqual(data["countries"][1]["code"], "FR")
795+
self.assertEqual(data["countries"][2]["code"], "CH")
796+
797+
def test_complex_accept_language_header(self):
798+
"""
799+
Test browser with multiple languages and quality values.
800+
801+
Modern browsers send complex Accept-Language headers like:
802+
'es-MX,es;q=0.9,en-US;q=0.8,en;q=0.7'
803+
804+
LocaleMiddleware parses quality values and picks the highest priority
805+
language, which should be es-MX, showing MX first in the country list.
806+
807+
Note: Django's default LANGUAGES includes 'es-mx'.
808+
"""
809+
with self.settings(
810+
COUNTRIES_FIRST_AUTO_DETECT=True,
811+
COUNTRIES_FIRST=["US", "GB"],
812+
MIDDLEWARE=[
813+
"django.middleware.common.CommonMiddleware",
814+
"django.middleware.locale.LocaleMiddleware",
815+
],
816+
ROOT_URLCONF="django_countries.tests.test_dynamic_first",
817+
):
818+
response = self.client.get(
819+
"/countries/",
820+
HTTP_ACCEPT_LANGUAGE="es-MX,es;q=0.9,en-US;q=0.8,en;q=0.7",
821+
)
822+
823+
self.assertEqual(response.status_code, 200)
824+
data = response.json()
825+
826+
# Should pick es-mx (highest priority)
827+
self.assertEqual(data["language"], "es-mx")
828+
829+
# MX should be prepended to COUNTRIES_FIRST
830+
self.assertEqual(data["countries"][0]["code"], "MX")
831+
self.assertEqual(data["countries"][1]["code"], "US")
832+
self.assertEqual(data["countries"][2]["code"], "GB")
833+
834+
835+
urlpatterns = test_urlpatterns # Make urlpatterns available at module level

0 commit comments

Comments
 (0)