@@ -88,6 +88,40 @@ def test_build_agent_override_merges_existing_options():
8888 == "enabled=1,fstrim_cloned_disks=1,type=virtio"
8989 )
9090 assert _build_agent_override ({"agent" : "enabled=0,type=virtio" }) == "enabled=1,type=virtio"
91+ # #222: normalized through mapping_from_response, so a {"data": ...} envelope
92+ # (or a non-dict) is handled defensively instead of raising AttributeError.
93+ assert _build_agent_override ({"data" : {"agent" : "type=virtio" }}) == "enabled=1,type=virtio"
94+ assert _build_agent_override ("not-a-dict" ) == "enabled=1" # type: ignore[arg-type]
95+
96+
97+ def test_cloud_init_password_is_length_bounded () -> None :
98+ """#222: CloudInitPayload.password must be bounded (max 128) so an oversized
99+ cipassword is rejected client-side rather than bloating the VM config or
100+ producing an opaque Proxmox error."""
101+ import pytest as _pytest
102+ from pydantic import ValidationError
103+
104+ CloudInitPayload (password = "x" * 128 ) # ok at the boundary
105+ with _pytest .raises (ValidationError ):
106+ CloudInitPayload (password = "x" * 129 )
107+
108+
109+ @pytest .mark .asyncio
110+ async def test_step_rollback_scrubs_cipassword_even_without_lease () -> None :
111+ """#222: the default enforce_cloud_network=False path (lease is None) must
112+ still route exceptions through _proxmox_step_failed so the cipassword is
113+ redacted from the client 502 body — parity with the SSE stream."""
114+ from fastapi import HTTPException
115+
116+ async def _boom () -> None :
117+ raise RuntimeError ("proxmox rejected cipassword=SuperSecret123 on config.put" )
118+
119+ with pytest .raises (HTTPException ) as exc :
120+ await provision_route ._run_proxmox_step_with_cloud_network_rollback (
121+ "configure_cloud_init" , _boom (), None
122+ )
123+ assert exc .value .status_code == 502
124+ assert "SuperSecret123" not in repr (exc .value .detail )
91125
92126
93127@pytest .mark .asyncio
0 commit comments