From b7b0d1b4a5fe8e7b53e7f56f49ddb475c15e386f Mon Sep 17 00:00:00 2001 From: Kawa Date: Fri, 10 Apr 2026 13:36:07 +0200 Subject: [PATCH] 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) --- imptune/api/scripts.py | 94 ++++++++++++++++++++++++++++++++++ imptune/main.py | 3 +- tests/test_script_generator.py | 77 ++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 imptune/api/scripts.py diff --git a/imptune/api/scripts.py b/imptune/api/scripts.py new file mode 100644 index 0000000..a5375b6 --- /dev/null +++ b/imptune/api/scripts.py @@ -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"'}, + ) diff --git a/imptune/main.py b/imptune/main.py index b093308..5ff3eb4 100644 --- a/imptune/main.py +++ b/imptune/main.py @@ -5,7 +5,7 @@ from pathlib import Path from fastapi import FastAPI 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.db.database import db, init_db @@ -33,3 +33,4 @@ app.include_router(pages.router) app.include_router(drivers.router) app.include_router(printers.router) app.include_router(clients.router) +app.include_router(scripts.router) diff --git a/tests/test_script_generator.py b/tests/test_script_generator.py index 4479283..2175ecd 100644 --- a/tests/test_script_generator.py +++ b/tests/test_script_generator.py @@ -3,6 +3,30 @@ import pytest 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( printer_name="HP LaserJet 4000", ip_address="192.168.1.10", @@ -118,3 +142,56 @@ def test_render_detect(): assert "Write-Output" in result assert "exit 0" 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