ixg_platform/apps/core/tests.py
ixgadmin d5bc06802e Serve map tiles from our own origin
The origin-zone map drew its background from a public tile server, which made it
the only part of the admin page that depended on a third-party domain — and the
part that breaks. Where a viewer's network, proxy or browser filter refuses
tile.openstreetmap.org (the refusal arrives as a 403) the map never draws, while
the page, the Leaflet bundle and the polygon data are all same-origin and fine.

Fetch the tiles server-side and serve them from /tiles/{z}/{x}/{y}.png: cached
on disk, so a page opened repeatedly does not re-fetch, and gated on being
signed in, so the route is not an open tile relay. The upstream stays
configurable via LEAFLET_TILE_URL, and LEAFLET_TILE_PROXY_ENABLED=False puts the
browser back on direct fetches.

When a tile still cannot be fetched, answer an empty tile rather than an error
page and say so under the map, instead of leaving a viewer with a blank grid and
no explanation.
2026-09-17 13:00:59 +00:00

155 lines
5.9 KiB
Python

"""Cross-cutting checks: every portal page renders, and map tiles are served
from our own origin.
Neither was covered before. The page sweep exists because a view can lose a
line — a `template_name`, say — and 500 in front of a user while the rest of the
suite stays green; it fetches each read-only page with an empty database, so it
covers the empty-state branches too. The tile tests cover the map's only
external dependency, which used to be fetched by the viewer's browser.
"""
import shutil
import tempfile
import urllib.error
from pathlib import Path
from unittest.mock import MagicMock, patch
from django.conf import settings
from django.contrib.auth import get_user_model
from django.test import Client, TestCase, override_settings
from .tiles import browser_tile_url, upstream_url
User = get_user_model()
OPS_ROUTES = [
"/ops/",
"/ops/batches/",
"/ops/qc/",
"/ops/qc/verify/",
"/ops/coa/",
"/ops/shipments/",
"/ops/capa/",
"/ops/documents/",
"/ops/esg/",
"/ops/eudr/",
"/ops/help/",
]
PORTAL_ROUTES = [
"/portal/",
"/portal/batches/",
"/portal/documents/",
"/portal/compliance/",
"/portal/subscription/",
"/portal/help/",
]
class PortalPagesRenderTests(TestCase):
def make_user(self, email, role):
return User.objects.create_user(
email=email, password="pass1234", role=role,
full_name=email.split("@")[0], is_staff=True,
)
def sweep(self, user, routes, host):
client = Client(SERVER_NAME=host)
client.force_login(user)
for route in routes:
with self.subTest(route=route):
response = client.get(route)
body = response.content
self.assertEqual(response.status_code, 200, f"{route} did not render")
for marker in (b"Traceback", b"TemplateDoesNotExist", b"Django Version"):
self.assertNotIn(marker, body, f"{route} rendered an error page")
def test_ops_pages_render(self):
self.sweep(self.make_user("ops-sweep@ixg.local", "ops"), OPS_ROUTES, "ops.ixgltd.com")
def test_buyer_pages_render(self):
self.sweep(self.make_user("buyer-sweep@ixg.local", "buyer"), PORTAL_ROUTES, "portal.ixgltd.com")
class TileProxyTests(TestCase):
"""Tiles come from our origin, so the map does not depend on the viewer's
network being able to reach a third-party tile server."""
def setUp(self):
self.cache = Path(tempfile.mkdtemp(prefix="ixg-tile-cache-"))
self.addCleanup(shutil.rmtree, self.cache, True)
override = override_settings(LEAFLET_TILE_CACHE_DIR=str(self.cache))
override.enable()
self.addCleanup(override.disable)
self.user = User.objects.create_user(
email="tile-viewer@ixg.local", password="pass1234", role="admin",
full_name="Tile Viewer", is_staff=True,
)
self.anon = Client(SERVER_NAME="app.ixgltd.com")
self.viewer = Client(SERVER_NAME="app.ixgltd.com")
self.viewer.force_login(self.user)
@staticmethod
def fake_upstream(payload):
"""Stand in for urllib.request.urlopen returning a tile."""
response = MagicMock()
response.read.return_value = payload
context = MagicMock()
context.__enter__.return_value = response
return lambda *a, **k: context
def test_a_viewer_must_be_signed_in(self):
self.assertEqual(self.anon.get("/tiles/13/4085/3448.png").status_code, 403)
def test_tiles_outside_the_zoom_range_are_refused(self):
self.assertEqual(self.viewer.get("/tiles/20/1/1.png").status_code, 404)
# x beyond the span of that zoom level
self.assertEqual(self.viewer.get("/tiles/1/5/1.png").status_code, 404)
def test_a_tile_is_fetched_from_the_upstream_and_served(self):
payload = b"\x89PNG\r\n\x1a\n fake tile"
with patch("apps.core.tiles.urllib.request.urlopen", self.fake_upstream(payload)):
r = self.viewer.get("/tiles/13/4085/3448.png")
self.assertEqual(r.status_code, 200)
self.assertEqual(r["Content-Type"], "image/png")
self.assertEqual(r.content, payload)
self.assertTrue((self.cache / "13" / "4085" / "3448.png").is_file())
def test_a_second_request_is_served_from_the_cache(self):
payload = b"\x89PNG\r\n\x1a\n cached tile"
first = MagicMock(side_effect=self.fake_upstream(payload))
with patch("apps.core.tiles.urllib.request.urlopen", first):
self.viewer.get("/tiles/13/4085/3448.png")
self.assertEqual(first.call_count, 1)
with patch("apps.core.tiles.urllib.request.urlopen") as second:
r = self.viewer.get("/tiles/13/4085/3448.png")
self.assertEqual(second.call_count, 0, "cache should have been used")
self.assertEqual(r.status_code, 200)
self.assertEqual(r.content, payload)
def test_an_unreachable_upstream_answers_an_empty_tile_not_an_error_page(self):
with patch(
"apps.core.tiles.urllib.request.urlopen",
side_effect=urllib.error.HTTPError("u", 403, "Forbidden", None, None),
):
r = self.viewer.get("/tiles/13/4085/3448.png")
self.assertEqual(r.status_code, 204)
self.assertEqual(r.content, b"")
def test_browser_tile_url_is_same_origin_when_the_proxy_is_on(self):
self.assertEqual(browser_tile_url(), "/tiles/{z}/{x}/{y}.png")
def test_browser_tile_url_is_the_upstream_when_the_proxy_is_off(self):
with override_settings(LEAFLET_TILE_PROXY_ENABLED=False):
self.assertEqual(browser_tile_url(), settings.LEAFLET_TILE_URL)
def test_the_upstream_url_gets_the_coordinates_substituted(self):
with override_settings(
LEAFLET_TILE_URL="https://example.invalid/{s}/{z}/{x}/{y}.png"
):
self.assertEqual(
upstream_url(13, 4085, 3448),
"https://example.invalid/a/13/4085/3448.png",
)