feat(session): per-owner printer/config storage via cookie-scoped bearer key
Printers and groups (Client) are now scoped to an Owner identified by an opaque
bearer key (secrets.token_urlsafe(32)) stored in an httponly cookie, defaulting
to temporary. First-visit modal offers backup-key download (marks permanent) or
temporary-only choice. /session/restore re-attaches a fresh browser to a saved
key. Every printer-facing route enforces ownership (404 on mismatch, not just
filtering) since printer IDs are sequential ints. Drivers stay global/shared.
On upgrade, pre-existing printer/client rows backfill to a synthetic legacy Owner;
its key is written to {DATA_DIR}/legacy_owner_key.txt for manual restore.
SECURITY: Added Origin/Referer same-origin check on POST /session/restore to
block login-CSRF/session-fixation attacks (cross-site form POST can't re-point
victim's cookie at attacker's Owner without hitting that check first).
Tests: 140 pass (2 deselected: pre-existing locale-flaky, unrelated to this change).
Verified live: modal on first visit, isolation between browsers, backup-key
download and restore flow work end-to-end.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,21 @@ def client(tmp_data_dir):
|
||||
yield c
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def owner(client):
|
||||
"""The Owner the `client` fixture's cookie jar is scoped to.
|
||||
|
||||
Triggers the session middleware (any non-/health request creates the
|
||||
cookie), then resolves the Owner record so tests can create Printer/Client
|
||||
rows directly via the ORM that the same `client` can then see over HTTP.
|
||||
"""
|
||||
from imptune.db.models import Owner
|
||||
from imptune.services.session import COOKIE_NAME
|
||||
|
||||
client.get("/")
|
||||
return Owner.get(Owner.key == client.cookies[COOKIE_NAME])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmp_data_dir(tmp_path, monkeypatch):
|
||||
"""Set DATA_DIR to a temp directory so tests don't write to /data."""
|
||||
|
||||
@@ -34,6 +34,10 @@ def live_server(tmp_path_factory):
|
||||
os.makedirs(cfg.DRIVERS_DIR, exist_ok=True)
|
||||
os.makedirs(cfg.ICONS_DIR, exist_ok=True)
|
||||
|
||||
# Plain HTTP loopback server — Secure-flagged cookies would be dropped
|
||||
# inconsistently across browser engines, so disable that flag for E2E.
|
||||
cfg.COOKIE_SECURE = False
|
||||
|
||||
# Also set env var so lifespan handler picks up correct dirs
|
||||
os.environ["DATA_DIR"] = str(data_dir)
|
||||
|
||||
@@ -68,3 +72,31 @@ def live_server(tmp_path_factory):
|
||||
|
||||
server.should_exit = True
|
||||
thread.join(timeout=2.0)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def _e2e_owner_key(live_server):
|
||||
"""One Owner shared by every E2E browser context.
|
||||
|
||||
E2E specs cover unrelated UI (theme, i18n, port autofill); without a
|
||||
pre-existing cookie each fresh Playwright context would be treated as a
|
||||
first-time visitor and blocked by the onboarding modal (a real <dialog>
|
||||
that intercepts all pointer events until dismissed).
|
||||
"""
|
||||
from imptune.services.session import COOKIE_NAME
|
||||
|
||||
resp = httpx.get(f"{live_server}/")
|
||||
return resp.cookies[COOKIE_NAME]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _seed_owner_cookie(page, live_server, _e2e_owner_key):
|
||||
"""Pre-seed the owner cookie so the first-visit modal never appears in specs unrelated to onboarding."""
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from imptune.services.session import COOKIE_NAME
|
||||
|
||||
hostname = urlparse(live_server).hostname
|
||||
page.context.add_cookies(
|
||||
[{"name": COOKIE_NAME, "value": _e2e_owner_key, "domain": hostname, "path": "/"}]
|
||||
)
|
||||
|
||||
@@ -4,12 +4,16 @@ from __future__ import annotations
|
||||
import pytest
|
||||
|
||||
|
||||
def test_printer_edit_modal_open_and_prefill(page, live_server: str) -> None:
|
||||
def test_printer_edit_modal_open_and_prefill(page, live_server: str, _e2e_owner_key: str) -> None:
|
||||
"""Edit button opens modal with printer's current name pre-filled."""
|
||||
import httpx
|
||||
|
||||
# Create a printer via API
|
||||
with httpx.Client(base_url=live_server, follow_redirects=True) as api:
|
||||
from imptune.services.session import COOKIE_NAME
|
||||
|
||||
# Create a printer via API, under the same owner the page's cookie is seeded with
|
||||
with httpx.Client(
|
||||
base_url=live_server, follow_redirects=True, cookies={COOKIE_NAME: _e2e_owner_key}
|
||||
) as api:
|
||||
api.post(
|
||||
"/printers",
|
||||
data={
|
||||
@@ -30,11 +34,15 @@ def test_printer_edit_modal_open_and_prefill(page, live_server: str) -> None:
|
||||
assert name_val == "EditTest Printer"
|
||||
|
||||
|
||||
def test_printer_edit_submit_updates_list(page, live_server: str) -> None:
|
||||
def test_printer_edit_submit_updates_list(page, live_server: str, _e2e_owner_key: str) -> None:
|
||||
"""Submitting the edit form updates the printer name in the list (no page reload)."""
|
||||
import httpx
|
||||
|
||||
with httpx.Client(base_url=live_server, follow_redirects=True) as api:
|
||||
from imptune.services.session import COOKIE_NAME
|
||||
|
||||
with httpx.Client(
|
||||
base_url=live_server, follow_redirects=True, cookies={COOKIE_NAME: _e2e_owner_key}
|
||||
) as api:
|
||||
resp = api.post(
|
||||
"/printers",
|
||||
data={
|
||||
|
||||
@@ -43,6 +43,7 @@ def test_create_tables(db_env):
|
||||
init_db()
|
||||
|
||||
tables = db.get_tables()
|
||||
assert "owner" in tables
|
||||
assert "client" in tables
|
||||
assert "driver" in tables
|
||||
assert "printer" in tables
|
||||
|
||||
+12
-11
@@ -28,7 +28,7 @@ def _make_jpeg(width: int = 256, height: int = 256) -> bytes:
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def _create_printer(client):
|
||||
def _create_printer(owner):
|
||||
"""Create a test Printer record and return it."""
|
||||
from imptune.db.models import Printer
|
||||
|
||||
@@ -36,15 +36,16 @@ def _create_printer(client):
|
||||
name="Test Printer",
|
||||
ip_address="10.0.0.1",
|
||||
port_name="IP_10.0.0.1",
|
||||
owner=owner,
|
||||
)
|
||||
|
||||
|
||||
class TestIconUpload:
|
||||
def test_upload_valid_png(self, client, tmp_data_dir):
|
||||
def test_upload_valid_png(self, client, owner, tmp_data_dir):
|
||||
"""POST a valid 256x256 PNG returns 200 and Icon record created."""
|
||||
from imptune.db.models import Icon
|
||||
|
||||
printer = _create_printer(client)
|
||||
printer = _create_printer(owner)
|
||||
png_data = _make_png(256, 256)
|
||||
response = client.post(
|
||||
f"/printers/{printer.id}/icon",
|
||||
@@ -65,9 +66,9 @@ class TestIconUpload:
|
||||
icon_file = Path(tmp_data_dir) / "icons" / sha256
|
||||
assert icon_file.exists()
|
||||
|
||||
def test_reject_non_png(self, client, tmp_data_dir):
|
||||
def test_reject_non_png(self, client, owner, tmp_data_dir):
|
||||
"""POST with a JPEG file returns 422 with PNG format error."""
|
||||
printer = _create_printer(client)
|
||||
printer = _create_printer(owner)
|
||||
jpeg_data = _make_jpeg(256, 256)
|
||||
response = client.post(
|
||||
f"/printers/{printer.id}/icon",
|
||||
@@ -76,9 +77,9 @@ class TestIconUpload:
|
||||
assert response.status_code == 422
|
||||
assert "PNG" in response.text
|
||||
|
||||
def test_reject_oversized(self, client, tmp_data_dir):
|
||||
def test_reject_oversized(self, client, owner, tmp_data_dir):
|
||||
"""POST with PNG > 750KB returns 422 with 750 KB error."""
|
||||
printer = _create_printer(client)
|
||||
printer = _create_printer(owner)
|
||||
# Craft oversized data: valid PNG bytes followed by padding
|
||||
png_bytes = _make_png(256, 256)
|
||||
oversized = png_bytes + b"\x00" * (750 * 1024 + 1 - len(png_bytes))
|
||||
@@ -89,9 +90,9 @@ class TestIconUpload:
|
||||
assert response.status_code == 422
|
||||
assert "750" in response.text
|
||||
|
||||
def test_reject_wrong_dimensions(self, client, tmp_data_dir):
|
||||
def test_reject_wrong_dimensions(self, client, owner, tmp_data_dir):
|
||||
"""POST with 128x128 PNG returns 422 with 256x256 error."""
|
||||
printer = _create_printer(client)
|
||||
printer = _create_printer(owner)
|
||||
png_data = _make_png(128, 128)
|
||||
response = client.post(
|
||||
f"/printers/{printer.id}/icon",
|
||||
@@ -100,11 +101,11 @@ class TestIconUpload:
|
||||
assert response.status_code == 422
|
||||
assert "256x256" in response.text
|
||||
|
||||
def test_replace_existing_icon(self, client, tmp_data_dir):
|
||||
def test_replace_existing_icon(self, client, owner, tmp_data_dir):
|
||||
"""Second upload for same printer replaces the Icon record."""
|
||||
from imptune.db.models import Icon
|
||||
|
||||
printer = _create_printer(client)
|
||||
printer = _create_printer(owner)
|
||||
|
||||
# First upload
|
||||
png1 = _make_png(256, 256)
|
||||
|
||||
@@ -22,7 +22,7 @@ def driver_zip_bytes():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def setup_printer_with_driver(tmp_data_dir, driver_zip_bytes):
|
||||
def setup_printer_with_driver(tmp_data_dir, driver_zip_bytes, owner):
|
||||
"""Create Driver record (with ZIP on disk) and Printer record linked to it."""
|
||||
import hashlib
|
||||
import os
|
||||
@@ -50,6 +50,7 @@ def setup_printer_with_driver(tmp_data_dir, driver_zip_bytes):
|
||||
ip_address="192.168.1.100",
|
||||
port_name="IP_192.168.1.100",
|
||||
driver=driver,
|
||||
owner=owner,
|
||||
duplex_mode="OneSided",
|
||||
color_mode=True,
|
||||
paper_size="A4",
|
||||
@@ -59,7 +60,7 @@ def setup_printer_with_driver(tmp_data_dir, driver_zip_bytes):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def printer_no_driver(tmp_data_dir):
|
||||
def printer_no_driver(tmp_data_dir, owner):
|
||||
"""Create Printer record with no driver assigned."""
|
||||
from imptune.db.models import Printer
|
||||
|
||||
@@ -68,6 +69,7 @@ def printer_no_driver(tmp_data_dir):
|
||||
ip_address="10.0.0.1",
|
||||
port_name="IP_10.0.0.1",
|
||||
driver=None,
|
||||
owner=owner,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ def test_create_printer_invalid_ip(client: TestClient) -> None:
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_printer_detail_shows_driver(client: TestClient) -> None:
|
||||
def test_printer_detail_shows_driver(client: TestClient, owner) -> None:
|
||||
"""GET /printers/{id} returns 200 with all printer fields and driver name."""
|
||||
from imptune.db.models import Driver, Printer
|
||||
|
||||
@@ -204,6 +204,7 @@ def test_printer_detail_shows_driver(client: TestClient) -> None:
|
||||
ip_address="10.0.1.1",
|
||||
port_name="IP_10_0_1_1",
|
||||
driver=driver_obj,
|
||||
owner=owner,
|
||||
)
|
||||
|
||||
resp = client.get(f"/printers/{printer.id}")
|
||||
@@ -220,7 +221,7 @@ def test_printer_detail_not_found(client: TestClient) -> None:
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_printer_detail_no_driver(client: TestClient) -> None:
|
||||
def test_printer_detail_no_driver(client: TestClient, owner) -> None:
|
||||
"""GET /printers/{id} for printer with no driver returns 200 with 'No driver assigned'."""
|
||||
from imptune.db.models import Printer
|
||||
|
||||
@@ -229,6 +230,7 @@ def test_printer_detail_no_driver(client: TestClient) -> None:
|
||||
ip_address="10.0.1.2",
|
||||
port_name="IP_10_0_1_2",
|
||||
driver=None,
|
||||
owner=owner,
|
||||
)
|
||||
|
||||
resp = client.get(f"/printers/{printer.id}")
|
||||
@@ -311,7 +313,7 @@ def test_printers_library_no_form(client: TestClient) -> None:
|
||||
assert 'action="/printers" method="post"' not in html
|
||||
|
||||
|
||||
def test_patch_printer(client: TestClient) -> None:
|
||||
def test_patch_printer(client: TestClient, owner) -> None:
|
||||
"""PATCH /printers/{id} with updated name returns 200, updated name in response, DB updated."""
|
||||
from imptune.db.models import Printer
|
||||
|
||||
@@ -319,6 +321,7 @@ def test_patch_printer(client: TestClient) -> None:
|
||||
name="Original Name",
|
||||
ip_address="10.0.2.1",
|
||||
port_name="IP_10_0_2_1",
|
||||
owner=owner,
|
||||
)
|
||||
|
||||
resp = client.patch(
|
||||
@@ -339,18 +342,19 @@ def test_patch_printer_not_found(client: TestClient) -> None:
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_client_detail_returns_200(client: TestClient) -> None:
|
||||
def test_client_detail_returns_200(client: TestClient, owner) -> None:
|
||||
"""GET /clients/{id} returns 200 with client name and assigned printer name."""
|
||||
from imptune.db.models import Client, Printer
|
||||
|
||||
# Create client
|
||||
cl = Client.create(name="Detail Client")
|
||||
cl = Client.create(name="Detail Client", owner=owner)
|
||||
# Create printer assigned to that client
|
||||
Printer.create(
|
||||
name="Client Printer",
|
||||
ip_address="10.0.3.1",
|
||||
port_name="IP_10_0_3_1",
|
||||
client=cl,
|
||||
owner=owner,
|
||||
)
|
||||
|
||||
resp = client.get(f"/clients/{cl.id}")
|
||||
@@ -366,7 +370,7 @@ def test_client_detail_not_found(client: TestClient) -> None:
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_client_links_in_printer_list(client: TestClient) -> None:
|
||||
def test_client_links_in_printer_list(client: TestClient, owner) -> None:
|
||||
"""GET /printers with a printer assigned to a client contains href to client detail."""
|
||||
from imptune.db.models import Client, Printer
|
||||
|
||||
@@ -382,6 +386,7 @@ def test_client_links_in_printer_list(client: TestClient) -> None:
|
||||
ip_address="10.0.4.1",
|
||||
port_name="IP_10_0_4_1",
|
||||
client=cl,
|
||||
owner=owner,
|
||||
)
|
||||
|
||||
resp = client.get("/printers")
|
||||
|
||||
@@ -22,7 +22,7 @@ def driver_zip_bytes():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def setup_printer_with_driver(tmp_data_dir, driver_zip_bytes):
|
||||
def setup_printer_with_driver(tmp_data_dir, driver_zip_bytes, owner):
|
||||
"""Create Driver record (with ZIP on disk) and Printer record linked to it."""
|
||||
import hashlib
|
||||
import os
|
||||
@@ -49,6 +49,7 @@ def setup_printer_with_driver(tmp_data_dir, driver_zip_bytes):
|
||||
ip_address="192.168.1.100",
|
||||
port_name="IP_192.168.1.100",
|
||||
driver=driver,
|
||||
owner=owner,
|
||||
duplex_mode="OneSided",
|
||||
color_mode=True,
|
||||
paper_size="A4",
|
||||
@@ -58,7 +59,7 @@ def setup_printer_with_driver(tmp_data_dir, driver_zip_bytes):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def printer_no_driver(tmp_data_dir):
|
||||
def printer_no_driver(tmp_data_dir, owner):
|
||||
"""Create Printer record with no driver assigned."""
|
||||
from imptune.db.models import Printer
|
||||
|
||||
@@ -67,6 +68,7 @@ def printer_no_driver(tmp_data_dir):
|
||||
ip_address="10.0.0.1",
|
||||
port_name="IP_10.0.0.1",
|
||||
driver=None,
|
||||
owner=owner,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import pytest
|
||||
from imptune.generators.script_generator import render_detect, render_install, render_uninstall
|
||||
|
||||
|
||||
def _create_test_driver_and_printer():
|
||||
def _create_test_driver_and_printer(owner):
|
||||
"""Helper: create a Driver + Printer for integration tests."""
|
||||
from imptune.db.models import Driver, Printer
|
||||
|
||||
@@ -19,6 +19,7 @@ def _create_test_driver_and_printer():
|
||||
ip_address="10.0.0.1",
|
||||
port_name="IP_10.0.0.1",
|
||||
driver=driver,
|
||||
owner=owner,
|
||||
duplex_mode="LongEdge",
|
||||
color_mode=True,
|
||||
paper_size="A4",
|
||||
@@ -149,25 +150,25 @@ def test_render_detect():
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_install_endpoint(client):
|
||||
def test_install_endpoint(client, owner):
|
||||
"""GET /printers/{id}/scripts/install returns 200 with pnputil in content."""
|
||||
_driver, printer = _create_test_driver_and_printer()
|
||||
_driver, printer = _create_test_driver_and_printer(owner)
|
||||
response = client.get(f"/printers/{printer.id}/scripts/install")
|
||||
assert response.status_code == 200
|
||||
assert "pnputil" in response.text
|
||||
|
||||
|
||||
def test_uninstall_endpoint(client):
|
||||
def test_uninstall_endpoint(client, owner):
|
||||
"""GET /printers/{id}/scripts/uninstall returns 200 with Remove-Printer in content."""
|
||||
_driver, printer = _create_test_driver_and_printer()
|
||||
_driver, printer = _create_test_driver_and_printer(owner)
|
||||
response = client.get(f"/printers/{printer.id}/scripts/uninstall")
|
||||
assert response.status_code == 200
|
||||
assert "Remove-Printer" in response.text
|
||||
|
||||
|
||||
def test_detect_endpoint(client):
|
||||
def test_detect_endpoint(client, owner):
|
||||
"""GET /printers/{id}/scripts/detect returns 200 with Write-Output in content."""
|
||||
_driver, printer = _create_test_driver_and_printer()
|
||||
_driver, printer = _create_test_driver_and_printer(owner)
|
||||
response = client.get(f"/printers/{printer.id}/scripts/detect")
|
||||
assert response.status_code == 200
|
||||
assert "Write-Output" in response.text
|
||||
@@ -179,7 +180,7 @@ def test_script_endpoint_missing_printer(client):
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_script_endpoint_no_driver(client):
|
||||
def test_script_endpoint_no_driver(client, owner):
|
||||
"""GET /printers/{id}/scripts/install returns 422 when no driver assigned."""
|
||||
from imptune.db.models import Printer
|
||||
|
||||
@@ -188,6 +189,7 @@ def test_script_endpoint_no_driver(client):
|
||||
ip_address="10.0.0.2",
|
||||
port_name="IP_10.0.0.2",
|
||||
driver=None,
|
||||
owner=owner,
|
||||
duplex_mode="OneSided",
|
||||
color_mode=True,
|
||||
paper_size="A4",
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Tests for per-owner cookie-scoped session — first visit, isolation, restore."""
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from imptune.services.session import COOKIE_NAME
|
||||
|
||||
|
||||
def test_first_visit_sets_cookie_and_new_owner_flag(client):
|
||||
resp = client.get("/")
|
||||
assert resp.status_code == 200
|
||||
assert COOKIE_NAME in client.cookies
|
||||
assert "session-choice-modal" in resp.text
|
||||
|
||||
# Second visit — cookie already present, modal must not reappear
|
||||
resp2 = client.get("/")
|
||||
assert "session-choice-modal" not in resp2.text
|
||||
|
||||
|
||||
def test_printers_are_isolated_between_owners(tmp_data_dir):
|
||||
from imptune.main import app
|
||||
|
||||
with TestClient(app) as client_a, TestClient(app) as client_b:
|
||||
client_a.post(
|
||||
"/printers",
|
||||
data={"name": "Owner A Printer", "ip_address": "10.0.0.1", "port_name": "IP_A"},
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
page_a = client_a.get("/printers")
|
||||
page_b = client_b.get("/printers")
|
||||
|
||||
assert "Owner A Printer" in page_a.text
|
||||
assert "Owner A Printer" not in page_b.text
|
||||
|
||||
|
||||
def test_owner_b_cannot_reach_owner_a_printer_by_id(tmp_data_dir):
|
||||
from imptune.main import app
|
||||
|
||||
with TestClient(app) as client_a, TestClient(app) as client_b:
|
||||
client_a.post(
|
||||
"/printers",
|
||||
data={"name": "Private Printer", "ip_address": "10.0.0.2", "port_name": "IP_B"},
|
||||
follow_redirects=False,
|
||||
)
|
||||
from imptune.db.models import Printer
|
||||
|
||||
printer_id = Printer.get(Printer.name == "Private Printer").id
|
||||
|
||||
assert client_b.get(f"/printers/{printer_id}").status_code == 404
|
||||
assert client_b.patch(f"/printers/{printer_id}", data={"name": "Hijacked"}).status_code == 404
|
||||
assert client_b.delete(f"/printers/{printer_id}").status_code == 404
|
||||
assert client_b.get(f"/printers/{printer_id}/scripts/install").status_code == 404
|
||||
assert client_b.get(f"/printers/{printer_id}/packages/ninja").status_code == 404
|
||||
|
||||
|
||||
def test_download_key_marks_permanent_and_returns_key(client):
|
||||
from imptune.db.models import Owner
|
||||
|
||||
client.get("/")
|
||||
resp = client.get("/session/key/download")
|
||||
assert resp.status_code == 200
|
||||
assert "attachment" in resp.headers["content-disposition"]
|
||||
assert "imptune-backup-key.txt" in resp.headers["content-disposition"]
|
||||
|
||||
key = resp.text
|
||||
assert len(key) > 20
|
||||
|
||||
owner = Owner.get(Owner.key == key)
|
||||
assert owner.is_permanent is True
|
||||
|
||||
|
||||
def test_restore_with_valid_key_reattaches_owner_data(tmp_data_dir):
|
||||
from imptune.main import app
|
||||
|
||||
with TestClient(app) as client_a:
|
||||
client_a.get("/")
|
||||
client_a.post(
|
||||
"/printers",
|
||||
data={"name": "Backed Up Printer", "ip_address": "10.0.0.3", "port_name": "IP_C"},
|
||||
follow_redirects=False,
|
||||
)
|
||||
key = client_a.get("/session/key/download").text
|
||||
|
||||
with TestClient(app) as client_new:
|
||||
restore_resp = client_new.post(
|
||||
"/session/restore",
|
||||
data={"key": key},
|
||||
headers={"origin": "http://testserver"},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert restore_resp.status_code == 303
|
||||
|
||||
page = client_new.get("/printers")
|
||||
assert "Backed Up Printer" in page.text
|
||||
|
||||
|
||||
def test_restore_with_invalid_key_shows_error(client):
|
||||
resp = client.post(
|
||||
"/session/restore",
|
||||
data={"key": "not-a-real-key"},
|
||||
headers={"origin": "http://testserver"},
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
assert "Key not found" in resp.text
|
||||
|
||||
|
||||
def test_restore_rejects_cross_origin_post(client):
|
||||
"""CSRF guard: a forged cross-site form POST must not be able to re-point
|
||||
the victim's cookie at an attacker-known key (login-CSRF / session fixation)."""
|
||||
resp = client.post(
|
||||
"/session/restore",
|
||||
data={"key": "irrelevant"},
|
||||
headers={"origin": "https://attacker.example"},
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
resp_no_header = client.post("/session/restore", data={"key": "irrelevant"})
|
||||
assert resp_no_header.status_code == 403
|
||||
|
||||
|
||||
def test_health_endpoint_does_not_create_owner_rows(client):
|
||||
from imptune.db.models import Owner
|
||||
|
||||
before = Owner.select().count()
|
||||
for _ in range(5):
|
||||
client.get("/health")
|
||||
after = Owner.select().count()
|
||||
assert after == before
|
||||
@@ -53,7 +53,7 @@ def test_theme_toggle_present(client):
|
||||
assert "cycle" in response.text or "store.theme" in response.text
|
||||
|
||||
|
||||
def test_dashboard_shows_recent_printers(client):
|
||||
def test_dashboard_shows_recent_printers(client, owner):
|
||||
"""Dashboard renders names of recently-created printers from DB."""
|
||||
from imptune.db.models import Printer
|
||||
|
||||
@@ -61,11 +61,13 @@ def test_dashboard_shows_recent_printers(client):
|
||||
name="TestPrinter-Alpha",
|
||||
ip_address="10.0.0.1",
|
||||
port_name="IP_10.0.0.1",
|
||||
owner=owner,
|
||||
)
|
||||
Printer.create(
|
||||
name="TestPrinter-Beta",
|
||||
ip_address="10.0.0.2",
|
||||
port_name="IP_10.0.0.2",
|
||||
owner=owner,
|
||||
)
|
||||
|
||||
response = client.get("/")
|
||||
@@ -85,7 +87,7 @@ def test_theme_toggle_present(client):
|
||||
assert "cycle" in response.text or "store.theme" in response.text
|
||||
|
||||
|
||||
def test_dashboard_shows_recent_packages(client):
|
||||
def test_dashboard_shows_recent_packages(client, owner):
|
||||
"""Dashboard recent-packages section shows only printers with a driver assigned."""
|
||||
from imptune.db.models import Driver, Printer
|
||||
|
||||
@@ -100,11 +102,13 @@ def test_dashboard_shows_recent_packages(client):
|
||||
ip_address="10.0.0.2",
|
||||
port_name="IP_10.0.0.2",
|
||||
driver=driver,
|
||||
owner=owner,
|
||||
)
|
||||
Printer.create(
|
||||
name="PkgPrinter-NoDriver",
|
||||
ip_address="10.0.0.3",
|
||||
port_name="IP_10.0.0.3",
|
||||
owner=owner,
|
||||
)
|
||||
|
||||
response = client.get("/")
|
||||
|
||||
@@ -18,7 +18,7 @@ def driver_zip_bytes():
|
||||
|
||||
|
||||
def test_upload_then_ninja_export_finds_driver_on_disk(
|
||||
client, tmp_data_dir, driver_zip_bytes
|
||||
client, tmp_data_dir, driver_zip_bytes, owner
|
||||
):
|
||||
from imptune.db.models import Client, Driver, Printer
|
||||
|
||||
@@ -33,13 +33,14 @@ def test_upload_then_ninja_export_finds_driver_on_disk(
|
||||
driver.driver_desc = json.dumps(["HP LaserJet Pro"])
|
||||
driver.save()
|
||||
|
||||
tenant = Client.create(name="Acme Corp")
|
||||
tenant = Client.create(name="Acme Corp", owner=owner)
|
||||
printer = Printer.create(
|
||||
name="Round Trip Printer",
|
||||
ip_address="192.168.1.50",
|
||||
port_name="IP_192.168.1.50",
|
||||
client=tenant,
|
||||
driver=driver,
|
||||
owner=owner,
|
||||
)
|
||||
|
||||
resp = client.get(f"/printers/{printer.id}/packages/ninja")
|
||||
|
||||
Reference in New Issue
Block a user