@@ -398,6 +398,37 @@ def test_create_template_backed_control_persists_resolved_defaults_when_values_o
398398 }
399399
400400
401+ def test_create_template_backed_control_failure_does_not_persist_control (
402+ client : TestClient ,
403+ ) -> None :
404+ # Given: an invalid template-backed create payload and the current control list
405+ invalid_payload = _template_payload ()
406+ invalid_payload ["template_values" ] = {"pattern" : "[" }
407+ control_name = f"invalid-template-control-{ uuid .uuid4 ()} "
408+
409+ before_response = client .get ("/api/v1/controls" )
410+ assert before_response .status_code == 200 , before_response .text
411+ before_controls = before_response .json ()["controls" ]
412+ before_ids = {control ["id" ] for control in before_controls }
413+
414+ # When: creating the invalid template-backed control
415+ response = client .put (
416+ "/api/v1/controls" ,
417+ json = {"name" : control_name , "data" : invalid_payload },
418+ )
419+
420+ # Then: the request fails and no control is persisted
421+ assert response .status_code == 422 , response .text
422+ assert response .json ()["error_code" ] == "TEMPLATE_PARAMETER_INVALID"
423+
424+ after_response = client .get ("/api/v1/controls" )
425+ assert after_response .status_code == 200 , after_response .text
426+ after_controls = after_response .json ()["controls" ]
427+ after_ids = {control ["id" ] for control in after_controls }
428+ assert after_ids == before_ids
429+ assert all (control ["name" ] != control_name for control in after_controls )
430+
431+
401432def test_template_backed_control_evaluates_after_policy_attachment (client : TestClient ) -> None :
402433 # Given: a template-backed control attached to an agent through a policy
403434 control_id , control_name = _create_template_control_with_name (client )
@@ -746,6 +777,36 @@ def test_raw_control_can_be_replaced_with_template_backed_control(client: TestCl
746777 assert data ["condition" ]["evaluator" ]["config" ]["pattern" ] == "hello"
747778
748779
780+ def test_raw_control_failed_template_replacement_does_not_mutate_raw_control (
781+ client : TestClient ,
782+ ) -> None :
783+ # Given: an existing raw control and invalid template-backed replacement input
784+ control_id = _create_raw_control (client )
785+ original_response = client .get (f"/api/v1/controls/{ control_id } /data" )
786+ assert original_response .status_code == 200 , original_response .text
787+ original_data = original_response .json ()["data" ]
788+
789+ invalid_payload = _template_payload ()
790+ invalid_payload ["template_values" ] = {"pattern" : "[" }
791+
792+ # When: replacing the raw control with invalid template-backed input
793+ response = client .put (
794+ f"/api/v1/controls/{ control_id } /data" ,
795+ json = {"data" : invalid_payload },
796+ )
797+
798+ # Then: the conversion fails and the raw control stays unchanged
799+ assert response .status_code == 422 , response .text
800+ assert response .json ()["error_code" ] == "TEMPLATE_PARAMETER_INVALID"
801+
802+ refreshed_response = client .get (f"/api/v1/controls/{ control_id } /data" )
803+ assert refreshed_response .status_code == 200 , refreshed_response .text
804+ refreshed_data = refreshed_response .json ()["data" ]
805+ assert refreshed_data == original_data
806+ assert "template" not in refreshed_data
807+ assert "template_values" not in refreshed_data
808+
809+
749810def test_template_update_preserves_enabled_value (client : TestClient ) -> None :
750811 # Given: a template-backed control that has been disabled through the metadata patch API
751812 control_id = _create_template_control (client )
@@ -781,6 +842,33 @@ def test_template_update_preserves_enabled_value(client: TestClient) -> None:
781842 assert data ["scope" ]["step_names" ] == ["updated-step" ]
782843
783844
845+ def test_template_update_failure_does_not_mutate_existing_template_backed_control (
846+ client : TestClient ,
847+ ) -> None :
848+ # Given: an existing template-backed control and invalid updated template values
849+ control_id = _create_template_control (client )
850+ original_response = client .get (f"/api/v1/controls/{ control_id } /data" )
851+ assert original_response .status_code == 200 , original_response .text
852+ original_data = original_response .json ()["data" ]
853+
854+ invalid_payload = _template_payload ()
855+ invalid_payload ["template_values" ] = {"pattern" : "[" }
856+
857+ # When: updating the stored template-backed control with invalid input
858+ response = client .put (
859+ f"/api/v1/controls/{ control_id } /data" ,
860+ json = {"data" : invalid_payload },
861+ )
862+
863+ # Then: the update fails and the stored control remains unchanged
864+ assert response .status_code == 422 , response .text
865+ assert response .json ()["error_code" ] == "TEMPLATE_PARAMETER_INVALID"
866+
867+ refreshed_response = client .get (f"/api/v1/controls/{ control_id } /data" )
868+ assert refreshed_response .status_code == 200 , refreshed_response .text
869+ assert refreshed_response .json ()["data" ] == original_data
870+
871+
784872def test_template_update_preview_matches_persisted_rendered_control (client : TestClient ) -> None :
785873 # Given: an existing template-backed control and updated template values
786874 control_id = _create_template_control (client )
0 commit comments