Skip to content

Commit 6c2059f

Browse files
committed
update test coverage
1 parent ce26f8f commit 6c2059f

File tree

2 files changed

+207
-9
lines changed

2 files changed

+207
-9
lines changed

servicex_app/servicex_app/resources/transformation/submit.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from servicex_app.did_parser import DIDParser
3939
from servicex_app.models import TransformRequest, db, TransformStatus
4040
from servicex_app.resources.servicex_resource import ServiceXResource
41-
from werkzeug.exceptions import BadRequest
41+
from werkzeug.exceptions import BadRequest, HTTPException
4242

4343

4444
def validate_custom_docker_image(image_name: str) -> bool:
@@ -251,14 +251,9 @@ def post(self):
251251
selection = json.loads(args["selection"])
252252
if "docker_image" in selection:
253253
custom_docker_image = selection["docker_image"]
254-
try:
255-
validate_custom_docker_image(custom_docker_image)
256-
except BadRequest as e:
257-
current_app.logger.error(
258-
str(e), extra={"requestId": request_id}
259-
)
254+
validate_custom_docker_image(custom_docker_image)
260255
except json.decoder.JSONDecodeError:
261-
pass
256+
raise BadRequest("Malformed JSON submitted")
262257

263258
if custom_docker_image:
264259
request_rec.image = custom_docker_image
@@ -310,6 +305,8 @@ def post(self):
310305
"Transformation request submitted!", extra={"requestId": request_id}
311306
)
312307
return {"request_id": str(request_id)}
308+
except HTTPException:
309+
raise
313310
except Exception as eek:
314311
current_app.logger.exception(
315312
"Got exception while submitting transformation request",

servicex_app/servicex_app_test/resources/transformation/test_submit.py

Lines changed: 202 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,22 @@
2525
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
2626
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2727
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28+
import json
29+
import os
2830
from datetime import datetime, timezone
29-
from unittest.mock import ANY
31+
from unittest.mock import ANY, patch
3032

33+
import pytest
3134
from celery import Celery
3235
from pytest import fixture
36+
from werkzeug.exceptions import BadRequest
3337

3438
from servicex_app import LookupResultProcessor
3539
from servicex_app.code_gen_adapter import CodeGenAdapter
3640
from servicex_app.dataset_manager import DatasetManager
3741
from servicex_app.models import Dataset
3842
from servicex_app.models import TransformRequest, DatasetStatus, TransformStatus
43+
from servicex_app.resources.transformation.submit import validate_custom_docker_image
3944
from servicex_app.transformer_manager import TransformerManager
4045
from servicex_app_test.resource_test_base import ResourceTestBase
4146

@@ -588,3 +593,199 @@ def test_submit_transformation_with_title(
588593
saved_obj = TransformRequest.lookup(request_id)
589594
assert saved_obj
590595
assert saved_obj.title == title
596+
597+
598+
class TestValidateCustomDockerImage:
599+
"""Tests for the validate_custom_docker_image function"""
600+
601+
def test_validate_with_matching_prefix(self):
602+
"""Test validation succeeds when image matches an allowed prefix"""
603+
with patch.dict(
604+
os.environ,
605+
{"TOPCP_ALLOWED_IMAGES": '["sslhep/servicex_science_image_topcp:"]'},
606+
):
607+
result = validate_custom_docker_image(
608+
"sslhep/servicex_science_image_topcp:2.17.0"
609+
)
610+
assert result is True
611+
612+
def test_validate_with_multiple_prefixes(self):
613+
"""Test validation with multiple allowed prefixes"""
614+
with patch.dict(
615+
os.environ,
616+
{
617+
"TOPCP_ALLOWED_IMAGES": '["sslhep/servicex_science_image_topcp:", "docker.io/ssl-hep/"]'
618+
},
619+
):
620+
assert (
621+
validate_custom_docker_image(
622+
"sslhep/servicex_science_image_topcp:latest"
623+
)
624+
is True
625+
)
626+
assert (
627+
validate_custom_docker_image("docker.io/ssl-hep/custom:v1") is True
628+
)
629+
630+
def test_validate_with_no_matching_prefix(self):
631+
"""Test validation fails when image doesn't match any allowed prefix"""
632+
with patch.dict(
633+
os.environ,
634+
{"TOPCP_ALLOWED_IMAGES": '["sslhep/servicex_science_image_topcp:"]'},
635+
):
636+
with pytest.raises(BadRequest, match="not allowed"):
637+
validate_custom_docker_image("unauthorized/image:latest")
638+
639+
def test_validate_with_no_env_variable(self):
640+
"""Test validation fails when TOPCP_ALLOWED_IMAGES is not set"""
641+
with patch.dict(os.environ, {}, clear=True):
642+
with pytest.raises(BadRequest, match="Custom Docker images are not allowed"):
643+
validate_custom_docker_image("sslhep/servicex_science_image_topcp:2.17.0")
644+
645+
def test_validate_with_invalid_json(self):
646+
"""Test validation fails with invalid JSON in env variable"""
647+
with patch.dict(os.environ, {"TOPCP_ALLOWED_IMAGES": "not-valid-json"}):
648+
with pytest.raises(BadRequest, match="improperly configured"):
649+
validate_custom_docker_image("sslhep/servicex_science_image_topcp:2.17.0")
650+
651+
def test_validate_with_non_list_json(self):
652+
"""Test validation fails when JSON is not a list"""
653+
with patch.dict(os.environ, {"TOPCP_ALLOWED_IMAGES": '{"key": "value"}'}):
654+
with pytest.raises(BadRequest, match="improperly configured"):
655+
validate_custom_docker_image("sslhep/servicex_science_image_topcp:2.17.0")
656+
657+
def test_validate_with_empty_list(self):
658+
"""Test validation fails when allowed list is empty"""
659+
with patch.dict(os.environ, {"TOPCP_ALLOWED_IMAGES": "[]"}):
660+
with pytest.raises(BadRequest, match="not allowed"):
661+
validate_custom_docker_image("sslhep/servicex_science_image_topcp:2.17.0")
662+
663+
664+
class TestSubmitTransformationRequestCustomImage(ResourceTestBase):
665+
"""Tests for custom Docker image feature in transformation requests"""
666+
667+
@staticmethod
668+
def _generate_transformation_request(**kwargs):
669+
request = {
670+
"did": "123-45-678",
671+
"selection": "test-string",
672+
"result-destination": "object-store",
673+
"result-format": "root-file",
674+
"workers": 10,
675+
"codegen": "topcp",
676+
}
677+
request.update(kwargs)
678+
return request
679+
680+
@fixture
681+
def mock_dataset_manager_from_did(self, mocker):
682+
dm = mocker.Mock()
683+
dm.dataset = Dataset(
684+
name="rucio://123-45-678",
685+
did_finder="rucio",
686+
lookup_status=DatasetStatus.looking,
687+
last_used=datetime.now(tz=timezone.utc),
688+
last_updated=datetime.fromtimestamp(0),
689+
)
690+
dm.name = "rucio://123-45-678"
691+
dm.id = 42
692+
mock_from_did = mocker.patch.object(DatasetManager, "from_did", return_value=dm)
693+
return mock_from_did
694+
695+
@fixture
696+
def mock_codegen(self, mocker):
697+
mock_code_gen = mocker.MagicMock(CodeGenAdapter)
698+
mock_code_gen.generate_code_for_selection.return_value = (
699+
"my-cm",
700+
"sslhep/servicex_science_image_topcp:2.17.0",
701+
"bash",
702+
"echo",
703+
)
704+
return mock_code_gen
705+
706+
def test_submit_topcp_with_custom_docker_image(
707+
self, mock_dataset_manager_from_did, mock_codegen, mock_app_version
708+
):
709+
"""Test submitting a TopCP transformation with a valid custom docker image"""
710+
extra_config = {
711+
"CODE_GEN_IMAGES": {"topcp": "sslhep/servicex_code_gen_topcp:develop"}
712+
}
713+
with patch.dict(
714+
os.environ,
715+
{"TOPCP_ALLOWED_IMAGES": '["sslhep/servicex_science_image_topcp:"]'},
716+
):
717+
client = self._test_client(code_gen_service=mock_codegen, extra_config=extra_config)
718+
with client.application.app_context():
719+
selection_dict = {"docker_image": "sslhep/servicex_science_image_topcp:custom"}
720+
request = self._generate_transformation_request(
721+
selection=json.dumps(selection_dict)
722+
)
723+
724+
response = client.post(
725+
"/servicex/transformation", json=request, headers=self.fake_header()
726+
)
727+
assert response.status_code == 200
728+
request_id = response.json["request_id"]
729+
730+
saved_obj = TransformRequest.lookup(request_id)
731+
assert saved_obj
732+
assert saved_obj.image == "sslhep/servicex_science_image_topcp:custom"
733+
734+
def test_submit_topcp_with_invalid_custom_docker_image(
735+
self, mock_dataset_manager_from_did, mock_codegen
736+
):
737+
"""Test submitting a TopCP transformation with an invalid custom docker image returns 400"""
738+
extra_config = {
739+
"CODE_GEN_IMAGES": {"topcp": "sslhep/servicex_code_gen_topcp:develop"}
740+
}
741+
with patch.dict(
742+
os.environ,
743+
{"TOPCP_ALLOWED_IMAGES": '["sslhep/servicex_science_image_topcp:"]'},
744+
):
745+
client = self._test_client(code_gen_service=mock_codegen, extra_config=extra_config)
746+
with client.application.app_context():
747+
selection_dict = {"docker_image": "unauthorized/image:latest"}
748+
request = self._generate_transformation_request(
749+
selection=json.dumps(selection_dict)
750+
)
751+
752+
response = client.post(
753+
"/servicex/transformation", json=request, headers=self.fake_header()
754+
)
755+
assert response.status_code == 400
756+
757+
def test_submit_topcp_with_non_json_selection(
758+
self, mock_dataset_manager_from_did, mock_codegen, mock_app_version
759+
):
760+
"""Test submitting a TopCP transformation with non-JSON selection string"""
761+
extra_config = {
762+
"CODE_GEN_IMAGES": {"topcp": "sslhep/servicex_code_gen_topcp:develop"}
763+
}
764+
client = self._test_client(code_gen_service=mock_codegen, extra_config=extra_config)
765+
with client.application.app_context():
766+
request = self._generate_transformation_request(selection="not-json-string")
767+
768+
response = client.post(
769+
"/servicex/transformation", json=request, headers=self.fake_header()
770+
)
771+
assert response.status_code == 400
772+
773+
def test_submit_custom_topcp_without_env_var(
774+
self, mock_dataset_manager_from_did, mock_codegen
775+
):
776+
"""Test submitting TopCP with custom image when TOPCP_ALLOWED_IMAGES is not set"""
777+
extra_config = {
778+
"CODE_GEN_IMAGES": {"topcp": "sslhep/servicex_code_gen_topcp:develop"}
779+
}
780+
with patch.dict(os.environ, {}, clear=True):
781+
client = self._test_client(code_gen_service=mock_codegen, extra_config=extra_config)
782+
with client.application.app_context():
783+
selection_dict = {"docker_image": "sslhep/servicex_science_image_topcp:custom"}
784+
request = self._generate_transformation_request(
785+
selection=json.dumps(selection_dict)
786+
)
787+
788+
response = client.post(
789+
"/servicex/transformation", json=request, headers=self.fake_header()
790+
)
791+
assert response.status_code == 400

0 commit comments

Comments
 (0)