diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9a7bd99..01287e1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,28 +10,18 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12-dev'] - django-version: ['3.2', '4.2', 'main'] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.15-dev'] + django-version: ['5.2', '6.0', 'main'] exclude: + - python-version: '3.10' + django-version: '6.0' - python-version: '3.11' - django-version: '3.2' - - python-version: '3.12-dev' - django-version: '3.2' - - - python-version: '3.11' - django-version: '4.0' - - python-version: '3.12-dev' - django-version: '4.0' - - - python-version: '3.12-dev' - django-version: '4.1' + django-version: '6.0' - - python-version: '3.8' - django-version: 'main' - - python-version: '3.9' - django-version: 'main' - python-version: '3.10' django-version: 'main' + - python-version: '3.11' + django-version: 'main' steps: - uses: actions/checkout@v3 diff --git a/Makefile b/Makefile index 5a87979..ea31c3f 100644 --- a/Makefile +++ b/Makefile @@ -1,29 +1,31 @@ TARGET?=tests -.PHONY: ruff example test coverage +.PHONY: ruff example test coverage generate-mmdb-fixtures ruff: ruff user_sessions example tests example: DJANGO_SETTINGS_MODULE=example.settings PYTHONPATH=. \ - django-admin.py runserver + django-admin runserver check: DJANGO_SETTINGS_MODULE=example.settings PYTHONPATH=. \ python -Wd example/manage.py check -generate-mmdb-fixtures: - docker --context=default buildx build -f tests/Dockerfile --tag test-mmdb-maker tests - docker run --rm --volume $$(pwd)/tests:/data test-mmdb-maker +$(TARGET)/test_city.mmdb $(TARGET)/test_country.mmdb: $(TARGET)/generate_mmdb.py + python3 $(TARGET)/generate_mmdb.py + stat $@ + +generate-mmdb-fixtures: $(TARGET)/test_city.mmdb $(TARGET)/test_country.mmdb test: generate-mmdb-fixtures DJANGO_SETTINGS_MODULE=tests.settings PYTHONPATH=. \ - django-admin.py test ${TARGET} + django-admin test ${TARGET} migrations: DJANGO_SETTINGS_MODULE=tests.settings PYTHONPATH=. \ - django-admin.py makemigrations user_sessions + django-admin makemigrations user_sessions coverage: coverage erase @@ -34,8 +36,8 @@ coverage: tx-pull: tx pull -a - cd user_sessions; django-admin.py compilemessages + cd user_sessions; django-admin compilemessages tx-push: - cd user_sessions; django-admin.py makemessages -l en + cd user_sessions; django-admin makemessages -l en tx push -s diff --git a/pyproject.toml b/pyproject.toml index 4aa149f..120e530 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2"] [project] name = "django-user-sessions" authors = [ - {name = "Bouke Haarsma", email = "bouke@haarsma.eu"}, + {name = "Bouke Haarsma", email = "bouke@haarsma.eu"}, ] description = "Django sessions with a foreign key to the user" readme = "README.rst" @@ -30,7 +30,7 @@ classifiers = [ "Topic :: Security", ] dependencies = [ - "Django>=3.2", + "Django>=3.2", ] dynamic = ["version"] @@ -58,6 +58,7 @@ dev = [ # Build "bumpversion", "twine", + "mmdb-writer", ] [tool.ruff] diff --git a/tests/Dockerfile b/tests/Dockerfile deleted file mode 100644 index d363dc6..0000000 --- a/tests/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM perl:latest - -RUN cpanm MaxMind::DB::Writer - -COPY generate_mmdb.pl / - -VOLUME ["/data"] - -CMD ["perl", "/generate_mmdb.pl"] diff --git a/tests/generate_mmdb.pl b/tests/generate_mmdb.pl deleted file mode 100755 index 2b2f5b6..0000000 --- a/tests/generate_mmdb.pl +++ /dev/null @@ -1,89 +0,0 @@ -#!perl - -use MaxMind::DB::Writer::Tree; - -my %city_types = ( - city => 'map', - code => 'utf8_string', - continent => 'map', - country => 'map', - en => 'utf8_string', - is_in_european_union => 'boolean', - iso_code => 'utf8_string', - latitude => 'double', - location => 'map', - longitude => 'double', - metro_code => 'utf8_string', - names => 'map', - postal => 'map', - subdivisions => ['array', 'map'], - region => 'utf8_string', - time_zone => 'utf8_string', -); - -my $city_tree = MaxMind::DB::Writer::Tree->new( - ip_version => 6, - record_size => 24, - database_type => 'GeoLite2-City', - languages => ['en'], - description => { en => 'Test database of IP city data' }, - map_key_type_callback => sub { $city_types{ $_[0] } }, -); - -$city_tree->insert_network( - '44.55.66.77/32', - { - city => { names => {en => 'San Diego'} }, - continent => { code => 'NA', names => {en => 'North America'} }, - country => { iso_code => 'US', names => {en => 'United States'} }, - is_in_european_union => false, - location => { - latitude => 37.751, - longitude => -97.822, - metro_code => 'custom metro code', - time_zone => 'America/Los Angeles', - }, - postal => { code => 'custom postal code' }, - subdivisions => [ - { iso_code => 'ABC', names => {en => 'Absolute Basic Class'} }, - ], - }, -); - -my $outfile = ($ENV{'DATA_DIR'} || '/data/') . ($ENV{'CITY_FILENAME'} || 'test_city.mmdb'); -open my $fh, '>:raw', $outfile; -$city_tree->write_tree($fh); - - - -my %country_types = ( - country => 'map', - iso_code => 'utf8_string', - names => 'map', - en => 'utf8_string', -); - -my $country_tree = MaxMind::DB::Writer::Tree->new( - ip_version => 6, - record_size => 24, - database_type => 'GeoLite2-Country', - languages => ['en'], - description => { en => 'Test database of IP country data' }, - map_key_type_callback => sub { $country_types{ $_[0] } }, -); - -$country_tree->insert_network( - '8.8.8.8/32', - { - country => { - iso_code => 'US', - names => { - en => 'United States', - }, - }, - }, -); - -my $outfile = ($ENV{'DATA_DIR'} || '/data/') . ($ENV{'COUNTRY_FILENAME'} || 'test_country.mmdb'); -open my $fh, '>:raw', $outfile; -$country_tree->write_tree($fh); diff --git a/tests/generate_mmdb.py b/tests/generate_mmdb.py new file mode 100644 index 0000000..b902a6c --- /dev/null +++ b/tests/generate_mmdb.py @@ -0,0 +1,85 @@ +from netaddr import IPSet + +from mmdb_writer import MMDBWriter + + +city_writer = MMDBWriter(database_type="GeoIP2-City") + +city_writer.insert_network( + IPSet(["44.55.66.77/32"]), + { + "city": { + "names": { + "en": "San Diego", + }, + }, + "continent": { + "code": "NA", + "names": { + "en": "North America", + }, + }, + "country": { + "iso_code": "US", + "names": { + "en": "United States", + }, + }, + "is_in_european_union": False, + "location": { + "latitude": 37.751, + "longitude": -97.822, + "metro_code": "custom metro code", + "time_zone": "America/Los Angeles", + }, + "postal": { + "code": "custom postal code", + }, + "subdivisions": [ + { + "iso_code": "ABC", + "names": { + "en": 'Absolute Basic Class', + }, + }, + ], + }, +) +city_writer.insert_network( + IPSet(["8.8.8.8/32"]), + { + "country": { + "iso_code": "US", + "names": { + "en": "United States", + }, + }, + }, +) +city_writer.to_db_file("tests/test_city.mmdb") + +country_writer = MMDBWriter(database_type="GeoIP2-Country") +country_writer.insert_network( + IPSet(["55.66.77.88/32"]), + { + "country": { + "iso_code": "US", + "names": { + "en": "United States", + }, + }, + }, +) +country_writer.insert_network( + IPSet(["8.8.4.4/32"]), + { + "country": { + "iso_code": "US", + "names": { + "en": "United States", + }, + }, + }, +) + +country_writer.to_db_file("tests/test_country.mmdb") diff --git a/tests/test_template_filters.py b/tests/test_template_filters.py index 4589596..2c79a44 100644 --- a/tests/test_template_filters.py +++ b/tests/test_template_filters.py @@ -3,6 +3,7 @@ from django.test import TestCase from django.test.utils import override_settings +from user_sessions.templatetags import user_sessions from user_sessions.templatetags.user_sessions import ( browser, city, country, device, location, platform, ) @@ -13,24 +14,23 @@ geoip = GeoIP2() geoip_msg = None except Exception as error_geoip2: # pragma: no cover - try: - from django.contrib.gis.geoip import GeoIP - geoip = GeoIP() - geoip_msg = None - except Exception as error_geoip: - geoip = None - geoip_msg = str(error_geoip2) + " and " + str(error_geoip) + geoip = None + geoip_msg = str(error_geoip2) class LocationTemplateFilterTest(TestCase): - @override_settings(GEOIP_PATH=None) + def setUp(self): + # Remove the cached GeoIP object, since it caches the database type and + # this fails when we switch between city and country MMDB files + user_sessions._geoip = None + + @override_settings(GEOIP_CITY="") def test_no_location(self): with self.assertWarnsRegex( UserWarning, r"The address 127\.0\.0\.1 is not in the database", ): - loc = location('127.0.0.1') - self.assertEqual(loc, None) + self.assertIsNone(location('127.0.0.1')) @skipUnless(geoip, geoip_msg) def test_city(self): @@ -46,6 +46,40 @@ def test_locations(self): self.assertEqual('San Diego, United States', location('44.55.66.77')) +@override_settings(GEOIP_CITY="doesnt_exist") +class CountryLocationTemplateFilterTest(TestCase): + def setUp(self): + # Remove the cached GeoIP object, since it caches the database type and + # this fails when we switch between city and country MMDB files + user_sessions._geoip = None + + def test_no_location(self): + with self.assertWarnsRegex( + UserWarning, + r"The address 127\.0\.0\.1 is not in the database", + ): + self.assertIsNone(location('127.0.0.1')) + + @skipUnless(geoip, geoip_msg) + def test_city(self): + self.assertIsNone(city('55.66.77.88')) + self.assertIsNone(city('8.8.4.4')) + # Make sure it isn't somehow pulling any IP addresses from the city + # database + self.assertIsNone(city('44.55.66.77')) + self.assertIsNone(city('8.8.8.8')) + + @skipUnless(geoip, geoip_msg) + def test_country(self): + self.assertEqual('United States', country('55.66.77.88')) + self.assertEqual('United States', country('8.8.4.4')) + + @skipUnless(geoip, geoip_msg) + def test_locations(self): + self.assertEqual('United States', location('55.66.77.88')) + self.assertEqual('United States', location('8.8.4.4')) + + class PlatformTemplateFilterTest(TestCase): def test_windows(self): # Generic Windows diff --git a/tox.ini b/tox.ini index 406b88a..e746c39 100644 --- a/tox.ini +++ b/tox.ini @@ -2,37 +2,36 @@ ; Minimum version of Tox minversion = 1.8 envlist = - ; https://docs.djangoproject.com/en/4.2/faq/install/#what-python-version-can-i-use-with-django - py{37}-dj32 - py{38,39,310}-dj32 - py{311,312}-dj{42,main} + ; https://docs.djangoproject.com/en/6.0/faq/install/#what-python-version-can-i-use-with-django + py3{10,11,12,13,14}-dj52 + py3{12,13,14}-dj{60,main} [gh-actions] python = - 3.8: py38 - 3.9: py39 3.10: py310 3.11: py311 3.12: py312 + 3.13: py313 + 3.14: py314 [gh-actions:env] DJANGO = - 3.2: dj32 - 4.2: dj42 + 5.2: dj52 + 6.0: dj60 main: djmain [testenv] commands = - make generate-mmdb-fixtures + python3 tests/generate_mmdb.py coverage run {envbindir}/django-admin test -v 2 --pythonpath=./ --settings=tests.settings coverage report coverage xml deps = coverage - dj32: Django>=3.2,<4.0 - dj42: Django>=4.2,<4.3 + dj52: Django>=5.2,<6.0 + dj60: Django>=6.0,<6.1 djmain: https://github.com/django/django/archive/main.tar.gz geoip2 + mmdb-writer ignore_outcome = djmain: True -allowlist_externals = make diff --git a/user_sessions/templatetags/user_sessions.py b/user_sessions/templatetags/user_sessions.py index a7b5249..cb2b8d7 100644 --- a/user_sessions/templatetags/user_sessions.py +++ b/user_sessions/templatetags/user_sessions.py @@ -107,7 +107,10 @@ def device(value): @register.filter def city(value): - location = geoip() and geoip().city(value) + try: + location = geoip() and geoip().city(value) + except Exception: + location = None if location and location['city']: return location['city'] return None @@ -115,7 +118,11 @@ def city(value): @register.filter def country(value): - location = geoip() and geoip().country(value) + try: + location = geoip() and geoip().country(value) + except Exception as e: + warnings.warn(str(e), stacklevel=2) + location = geoip() and geoip().city(value) if location and location['country_name']: return location['country_name'] return None