Files
ImpTune/tests/test_web_routes.py
T
kawaandClaude Opus 5 2c06806814 feat: driver rename, driver icons, web image + driver search
Driver rename and icons: `Driver.display_name` plus a `DriverIcon` table, both
global/shared like the `Driver` row they hang off, so a rename or an icon is
what every Owner sees. The rename/icon dialog keeps its forms as siblings
(nested forms are invalid HTML) and the icon routes return an `hx-swap-oob`
thumbnail refresh rather than re-rendering the table, which would tear the open
`<dialog>` out of the DOM.

Web image picker: `GET /web/images` renders a pickable grid for a printer or a
driver icon, with the search term prefilled from the entity name and editable.
Picking one downloads it server-side and normalizes it.

Driver download search: `GET /web/drivers` searches for a vendor-wide driver
(the term is rewritten into the vendor's real product name for 15 brands) or for
the exact model as typed. Links only — nothing is downloaded, and the fragment
says the results are unvetted.

Icon uploads no longer reject off-size or non-PNG files: `normalize_icon()`
letterboxes any decodable raster into a 256x256 PNG. An already-exact 256x256
PNG is returned byte-identical, because icon storage is content-addressed and
re-encoding would move the file on every save.

`fetch_image()` makes the request from the server, so `assert_fetchable()`
refuses any URL resolving to a private, loopback, or link-local address, and
re-runs on every redirect. ImpTune sits on the same LAN as the printers it
configures; an unguarded fetcher would be a port scanner for anyone who can
reach the UI.

DuckDuckGo is scraped, not called through an API — no key needed, but fragile,
so both search functions swallow parse failures and return [] instead of 500ing
a page. `WEB_SEARCH=false` disables every outbound request and hides the
controls, for air-gapped installs.

Also: one shared `Jinja2Templates` in `templating.py` instead of five per-router
instances, so a template global is declared once; `_add_missing_columns()` in
`database.py` adds new nullable columns to a pre-existing table, which
`create_tables(safe=True)` skips; `db_env` in test_db.py now closes its
connection on teardown, or the next test's ORM writes land in the previous
test's DB file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 10:44:47 +02:00

268 lines
9.8 KiB
Python

"""/web/images and /web/drivers — the HTMX fragments behind the search UIs.
`websearch.search_images` / `search_pages` are monkeypatched throughout: these
tests are about routing, ownership, and rendering, not about the scrape.
"""
from __future__ import annotations
import io
import json
import pytest
from PIL import Image
from imptune.services.websearch import ImageHit, PageHit
@pytest.fixture
def stub_images(monkeypatch):
"""Record the query the route asked for, return two canned hits."""
calls: list[tuple[str, int]] = []
def fake(query, limit=12):
calls.append((query, limit))
return [
ImageHit(
url="https://cdn.example.com/a.png",
thumbnail="https://tn.example.com/a.png",
title="Printer A",
width=800,
height=600,
source="cdn.example.com",
),
ImageHit(
url="https://cdn.example.com/b.jpg",
thumbnail="https://tn.example.com/b.jpg",
title="Printer B",
width=400,
height=400,
source="cdn.example.com",
),
]
from imptune.api import web
monkeypatch.setattr(web.websearch, "search_images", fake)
return calls
@pytest.fixture
def stub_pages(monkeypatch):
calls: list[tuple[str, int]] = []
def fake(query, limit=8):
calls.append((query, limit))
return [
PageHit(
url="https://support.hp.com/upd",
title="HP Universal Print Driver",
snippet="Download the UPD for Windows.",
host="support.hp.com",
)
]
from imptune.api import web
monkeypatch.setattr(web.websearch, "search_pages", fake)
return calls
def _printer(owner):
from imptune.db.models import Printer
return Printer.create(
name="HP LaserJet M404",
ip_address="10.0.0.7",
port_name="IP_10.0.0.7",
owner=owner,
)
def _driver():
from imptune.db.models import Driver
return Driver.create(
sha256="c" * 64,
original_filename="hp_upd.zip",
size_bytes=1024,
driver_desc=json.dumps(["HP Universal Printing PCL 6"]),
)
class TestImageSearchRoute:
def test_renders_pickable_results_for_a_printer(self, client, owner, tmp_data_dir, stub_images):
printer = _printer(owner)
response = client.get(
"/web/images", params={"q": "hp m404 printer", "target": "printer", "id": printer.id}
)
assert response.status_code == 200
assert stub_images == [("hp m404 printer", 12)]
assert f'hx-post="/printers/{printer.id}/icon/from-web"' in response.text
assert 'hx-target="#icon-status"' in response.text
assert "https://cdn.example.com/a.png" in response.text
# Thumbnails load from the engine's CDN — no referrer leak.
assert 'referrerpolicy="no-referrer"' in response.text
def test_renders_pickable_results_for_a_driver(self, client, owner, tmp_data_dir, stub_images):
driver = _driver()
response = client.get(
"/web/images", params={"q": "hp upd", "target": "driver", "id": driver.id}
)
assert response.status_code == 200
assert f'hx-post="/drivers/{driver.id}/icon/from-web"' in response.text
assert f'hx-target="#driver-icon-status-{driver.id}"' in response.text
def test_another_owners_printer_reads_as_missing(self, client, owner, tmp_data_dir, stub_images):
"""404, not 403 — and no search is spent on it."""
from imptune.db.models import Owner, Printer
from imptune.services.session import generate_key
stranger = Owner.create(key=generate_key())
theirs = Printer.create(
name="Theirs", ip_address="10.0.0.9", port_name="IP_10_0_0_9", owner=stranger
)
response = client.get(
"/web/images", params={"q": "x", "target": "printer", "id": theirs.id}
)
assert response.status_code == 404
assert stub_images == []
def test_unknown_driver_is_404(self, client, tmp_data_dir, stub_images):
response = client.get(
"/web/images", params={"q": "x", "target": "driver", "id": 99999}
)
assert response.status_code == 404
def test_unknown_target_is_400(self, client, tmp_data_dir, stub_images):
response = client.get("/web/images", params={"q": "x", "target": "wat", "id": 1})
assert response.status_code == 400
def test_blank_query_does_not_search(self, client, owner, tmp_data_dir, stub_images):
printer = _printer(owner)
response = client.get(
"/web/images", params={"q": " ", "target": "printer", "id": printer.id}
)
assert response.status_code == 200
assert stub_images == []
def test_no_results_renders_a_hint(self, client, owner, tmp_data_dir, monkeypatch):
from imptune.api import web
monkeypatch.setattr(web.websearch, "search_images", lambda q, limit=12: [])
printer = _printer(owner)
response = client.get(
"/web/images", params={"q": "zzz", "target": "printer", "id": printer.id}
)
assert response.status_code == 200
assert "No results" in response.text
def test_disabled_deployment_says_so(self, client, owner, tmp_data_dir, monkeypatch):
import imptune.config as cfg
monkeypatch.setattr(cfg, "WEB_SEARCH", False)
printer = _printer(owner)
response = client.get(
"/web/images", params={"q": "x", "target": "printer", "id": printer.id}
)
assert response.status_code == 200
assert "disabled" in response.text
class TestDriverSearchRoute:
def test_generic_mode_rewrites_the_query(self, client, tmp_data_dir, stub_pages):
response = client.get(
"/web/drivers", params={"q": "HP LaserJet M404dn", "mode": "generic"}
)
assert response.status_code == 200
assert stub_pages == [("HP Universal Print Driver PCL6 download", 12)]
assert "support.hp.com/upd" in response.text
def test_exact_mode_searches_the_text_as_typed(self, client, tmp_data_dir, stub_pages):
response = client.get(
"/web/drivers", params={"q": "HP LaserJet M404dn", "mode": "exact"}
)
assert stub_pages == [("HP LaserJet M404dn", 12)]
def test_results_carry_the_unvetted_warning(self, client, tmp_data_dir, stub_pages):
"""The links are raw search results — the fragment must say so."""
response = client.get("/web/drivers", params={"q": "hp", "mode": "generic"})
assert "driver_search_warning" in response.text
assert 'rel="noopener noreferrer nofollow"' in response.text
assert 'target="_blank"' in response.text
def test_blank_query_does_not_search(self, client, tmp_data_dir, stub_pages):
response = client.get("/web/drivers", params={"q": " "})
assert response.status_code == 200
assert stub_pages == []
def test_disabled_deployment_says_so(self, client, tmp_data_dir, monkeypatch):
import imptune.config as cfg
monkeypatch.setattr(cfg, "WEB_SEARCH", False)
response = client.get("/web/drivers", params={"q": "hp"})
assert "disabled" in response.text
class TestSearchUiVisibility:
def test_drivers_page_offers_the_search_card(self, client, tmp_data_dir):
page = client.get("/drivers")
assert 'hx-get="/web/drivers"' in page.text
assert "Konica Minolta" in page.text # brand datalist
def test_printer_detail_offers_the_image_picker(self, client, owner, tmp_data_dir):
printer = _printer(owner)
page = client.get(f"/printers/{printer.id}")
assert 'hx-get="/web/images"' in page.text
# Prefilled from the printer name, so one click is enough.
assert 'value="HP LaserJet M404 printer"' in page.text
def test_every_search_control_disappears_when_disabled(
self, client, owner, tmp_data_dir, monkeypatch
):
import imptune.config as cfg
monkeypatch.setattr(cfg, "WEB_SEARCH", False)
printer = _printer(owner)
assert 'hx-get="/web/drivers"' not in client.get("/drivers").text
assert 'hx-get="/web/images"' not in client.get(f"/printers/{printer.id}").text
def test_picked_image_becomes_the_printer_icon(client, owner, tmp_data_dir, monkeypatch):
"""End to end through the picker's POST: download, normalize, store, preview."""
from imptune.db.models import Icon
from imptune.services import websearch
printer = _printer(owner)
buf = io.BytesIO()
Image.new("RGB", (500, 300), color="orange").save(buf, format="JPEG")
monkeypatch.setattr(websearch, "fetch_image", lambda url: buf.getvalue())
response = client.post(
f"/printers/{printer.id}/icon/from-web",
data={"url": "https://cdn.example.com/photos/m404.jpg"},
)
assert response.status_code == 200
assert f'src="/printers/{printer.id}/icon?v=' in response.text
icon = Icon.get(Icon.printer == printer.id)
with Image.open(tmp_data_dir / "icons" / icon.sha256) as img:
assert img.format == "PNG"
assert img.size == (256, 256)
def test_from_web_rejects_an_unowned_printer(client, tmp_data_dir, monkeypatch):
from imptune.db.models import Owner, Printer
from imptune.services import websearch
from imptune.services.session import generate_key
monkeypatch.setattr(websearch, "fetch_image", lambda url: b"unused")
stranger = Owner.create(key=generate_key())
theirs = Printer.create(
name="Theirs", ip_address="10.0.0.9", port_name="IP_10_0_0_9", owner=stranger
)
response = client.post(
f"/printers/{theirs.id}/icon/from-web", data={"url": "https://example.com/x.png"}
)
assert response.status_code == 404