feat(04-02): add script download API endpoints and router registration

- imptune/api/scripts.py: GET /printers/{id}/scripts/{install,uninstall,detect}
- 404 for missing printer, 422 for missing driver/INF/driver_desc
- Content-Disposition attachment header on all responses
- imptune/main.py: register scripts.router
- 5 integration tests covering all endpoint cases (200/404/422)
This commit is contained in:
2026-04-10 13:36:07 +02:00
parent 6bff8f3c0c
commit b7b0d1b4a5
3 changed files with 173 additions and 1 deletions
+94
View File
@@ -0,0 +1,94 @@
"""Script download endpoints — generates and serves PowerShell scripts for a printer."""
import json
from fastapi import APIRouter
from fastapi.responses import PlainTextResponse
from imptune.db.models import Printer
from imptune.generators.script_generator import render_detect, render_install, render_uninstall
router = APIRouter(prefix="/printers")
def _get_printer_and_driver(printer_id: int):
"""Fetch printer and validate driver — returns (printer, driver_name) or PlainTextResponse error."""
printer = Printer.get_or_none(Printer.id == printer_id)
if printer is None:
return None, PlainTextResponse("Printer not found", status_code=404)
driver = printer.driver
if driver is None:
return None, PlainTextResponse("No driver assigned", status_code=422)
if not driver.inf_filename:
return None, PlainTextResponse("Driver has no INF file", status_code=422)
if not driver.driver_desc:
return None, PlainTextResponse("Driver has no description", status_code=422)
try:
desc_list = json.loads(driver.driver_desc)
driver_name = desc_list[0]
except (json.JSONDecodeError, IndexError, TypeError):
return None, PlainTextResponse("Driver description is invalid", status_code=422)
return (printer, driver, driver_name), None
@router.get("/{printer_id}/scripts/install")
def get_install_script(printer_id: int):
"""Download the PowerShell install script for a printer."""
result, error = _get_printer_and_driver(printer_id)
if error is not None:
return error
printer, driver, driver_name = result
rendered = render_install(
printer_name=printer.name,
ip_address=printer.ip_address,
port_name=printer.port_name,
driver_name=driver_name,
inf_filename=driver.inf_filename,
duplex_mode=printer.duplex_mode,
color_mode=printer.color_mode,
paper_size=printer.paper_size,
collate=printer.collate,
)
return PlainTextResponse(
content=rendered,
headers={"Content-Disposition": 'attachment; filename="install.ps1"'},
)
@router.get("/{printer_id}/scripts/uninstall")
def get_uninstall_script(printer_id: int):
"""Download the PowerShell uninstall script for a printer."""
result, error = _get_printer_and_driver(printer_id)
if error is not None:
return error
printer, driver, driver_name = result
rendered = render_uninstall(
printer_name=printer.name,
driver_name=driver_name,
port_name=printer.port_name,
)
return PlainTextResponse(
content=rendered,
headers={"Content-Disposition": 'attachment; filename="uninstall.ps1"'},
)
@router.get("/{printer_id}/scripts/detect")
def get_detect_script(printer_id: int):
"""Download the PowerShell detection script for a printer."""
result, error = _get_printer_and_driver(printer_id)
if error is not None:
return error
printer, driver, driver_name = result
rendered = render_detect(printer_name=printer.name)
return PlainTextResponse(
content=rendered,
headers={"Content-Disposition": 'attachment; filename="detect.ps1"'},
)
+2 -1
View File
@@ -5,7 +5,7 @@ from pathlib import Path
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from imptune.api import clients, drivers, health, pages, printers from imptune.api import clients, drivers, health, pages, printers, scripts
from imptune.config import DATA_DIR, DRIVERS_DIR from imptune.config import DATA_DIR, DRIVERS_DIR
from imptune.db.database import db, init_db from imptune.db.database import db, init_db
@@ -33,3 +33,4 @@ app.include_router(pages.router)
app.include_router(drivers.router) app.include_router(drivers.router)
app.include_router(printers.router) app.include_router(printers.router)
app.include_router(clients.router) app.include_router(clients.router)
app.include_router(scripts.router)
+77
View File
@@ -3,6 +3,30 @@ import pytest
from imptune.generators.script_generator import render_detect, render_install, render_uninstall from imptune.generators.script_generator import render_detect, render_install, render_uninstall
def _create_test_driver_and_printer():
"""Helper: create a Driver + Printer for integration tests."""
from imptune.db.models import Driver, Printer
driver = Driver.create(
sha256="abc123",
original_filename="test.zip",
size_bytes=100,
driver_desc='["Test Driver"]',
inf_filename="test.inf",
)
printer = Printer.create(
name="Test Printer",
ip_address="10.0.0.1",
port_name="IP_10.0.0.1",
driver=driver,
duplex_mode="LongEdge",
color_mode=True,
paper_size="A4",
collate=True,
)
return driver, printer
SAMPLE_ARGS = dict( SAMPLE_ARGS = dict(
printer_name="HP LaserJet 4000", printer_name="HP LaserJet 4000",
ip_address="192.168.1.10", ip_address="192.168.1.10",
@@ -118,3 +142,56 @@ def test_render_detect():
assert "Write-Output" in result assert "Write-Output" in result
assert "exit 0" in result assert "exit 0" in result
assert "exit 1" in result assert "exit 1" in result
# ---------------------------------------------------------------------------
# Integration tests — script API endpoints
# ---------------------------------------------------------------------------
def test_install_endpoint(client):
"""GET /printers/{id}/scripts/install returns 200 with pnputil in content."""
_driver, printer = _create_test_driver_and_printer()
response = client.get(f"/printers/{printer.id}/scripts/install")
assert response.status_code == 200
assert "pnputil" in response.text
def test_uninstall_endpoint(client):
"""GET /printers/{id}/scripts/uninstall returns 200 with Remove-Printer in content."""
_driver, printer = _create_test_driver_and_printer()
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):
"""GET /printers/{id}/scripts/detect returns 200 with Write-Output in content."""
_driver, printer = _create_test_driver_and_printer()
response = client.get(f"/printers/{printer.id}/scripts/detect")
assert response.status_code == 200
assert "Write-Output" in response.text
def test_script_endpoint_missing_printer(client):
"""GET /printers/{id}/scripts/install returns 404 for nonexistent printer."""
response = client.get("/printers/9999/scripts/install")
assert response.status_code == 404
def test_script_endpoint_no_driver(client):
"""GET /printers/{id}/scripts/install returns 422 when no driver assigned."""
from imptune.db.models import Printer
printer = Printer.create(
name="No Driver Printer",
ip_address="10.0.0.2",
port_name="IP_10.0.0.2",
driver=None,
duplex_mode="OneSided",
color_mode=True,
paper_size="A4",
collate=True,
)
response = client.get(f"/printers/{printer.id}/scripts/install")
assert response.status_code == 422