Skip to content

Commit 4352f7f

Browse files
authored
Merge branch 'main' into coderabbit
2 parents 2cc85d6 + 39d3462 commit 4352f7f

10 files changed

Lines changed: 605 additions & 101 deletions

File tree

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ repos:
3636
exclude: .*/__snapshots__/.*|.*-input\.json$
3737

3838
- repo: https://github.com/astral-sh/ruff-pre-commit
39-
rev: v0.15.2
39+
rev: v0.15.4
4040
hooks:
4141
- id: ruff
4242
- id: ruff-format
@@ -81,7 +81,7 @@ repos:
8181
pass_filenames: false
8282

8383
- repo: https://github.com/rhysd/actionlint
84-
rev: v1.7.4
84+
rev: v1.7.11
8585
hooks:
8686
- id: actionlint
8787

tests/model_serving/model_server/kserve/negative/conftest.py

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from urllib.parse import urlparse
44

55
import pytest
6-
from _pytest.fixtures import FixtureRequest
76
from kubernetes.dynamic import DynamicClient
87
from ocp_resources.inference_service import InferenceService
98
from ocp_resources.namespace import Namespace
@@ -15,20 +14,58 @@
1514
RuntimeTemplates,
1615
)
1716
from utilities.inference_utils import create_isvc
18-
from utilities.infra import get_pods_by_isvc_label
17+
from utilities.infra import create_ns, get_pods_by_isvc_label, s3_endpoint_secret
1918
from utilities.serving_runtime import ServingRuntimeFromTemplate
2019

2120

22-
@pytest.fixture(scope="class")
21+
@pytest.fixture(scope="package")
22+
def negative_test_namespace(
23+
admin_client: DynamicClient,
24+
unprivileged_client: DynamicClient,
25+
) -> Generator[Namespace, Any, Any]:
26+
"""Create a shared namespace for all negative tests."""
27+
with create_ns(
28+
admin_client=admin_client,
29+
unprivileged_client=unprivileged_client,
30+
name="negative-test-kserve",
31+
) as ns:
32+
yield ns
33+
34+
35+
@pytest.fixture(scope="package")
36+
def negative_test_s3_secret(
37+
unprivileged_client: DynamicClient,
38+
negative_test_namespace: Namespace,
39+
aws_access_key_id: str,
40+
aws_secret_access_key: str,
41+
ci_s3_bucket_name: str,
42+
ci_s3_bucket_region: str,
43+
ci_s3_bucket_endpoint: str,
44+
) -> Generator[Secret, Any, Any]:
45+
"""Create S3 secret shared across all negative tests."""
46+
with s3_endpoint_secret(
47+
client=unprivileged_client,
48+
name="ci-bucket-secret",
49+
namespace=negative_test_namespace.name,
50+
aws_access_key=aws_access_key_id,
51+
aws_secret_access_key=aws_secret_access_key,
52+
aws_s3_region=ci_s3_bucket_region,
53+
aws_s3_bucket=ci_s3_bucket_name,
54+
aws_s3_endpoint=ci_s3_bucket_endpoint,
55+
) as secret:
56+
yield secret
57+
58+
59+
@pytest.fixture(scope="package")
2360
def ovms_serving_runtime(
2461
admin_client: DynamicClient,
25-
unprivileged_model_namespace: Namespace,
62+
negative_test_namespace: Namespace,
2663
) -> Generator[ServingRuntime, Any, Any]:
27-
"""Create OVMS serving runtime for negative tests."""
64+
"""Create OVMS serving runtime shared across all negative tests."""
2865
with ServingRuntimeFromTemplate(
2966
client=admin_client,
3067
name="negative-test-ovms-runtime",
31-
namespace=unprivileged_model_namespace.name,
68+
namespace=negative_test_namespace.name,
3269
template_name=RuntimeTemplates.OVMS_KSERVE,
3370
multi_model=False,
3471
enable_http=True,
@@ -37,27 +74,26 @@ def ovms_serving_runtime(
3774
yield runtime
3875

3976

40-
@pytest.fixture(scope="class")
77+
@pytest.fixture(scope="package")
4178
def negative_test_ovms_isvc(
42-
request: FixtureRequest,
4379
admin_client: DynamicClient,
44-
unprivileged_model_namespace: Namespace,
80+
negative_test_namespace: Namespace,
4581
ovms_serving_runtime: ServingRuntime,
4682
ci_s3_bucket_name: str,
47-
ci_endpoint_s3_secret: Secret,
83+
negative_test_s3_secret: Secret,
4884
) -> Generator[InferenceService, Any, Any]:
49-
"""Create InferenceService with OVMS runtime for negative tests."""
50-
storage_uri = f"s3://{ci_s3_bucket_name}/{request.param['model-dir']}/"
85+
"""Create InferenceService with OVMS runtime shared across all negative tests."""
86+
storage_uri = f"s3://{ci_s3_bucket_name}/test-dir/"
5187
supported_formats = ovms_serving_runtime.instance.spec.supportedModelFormats
5288
if not supported_formats:
5389
raise ValueError(f"ServingRuntime '{ovms_serving_runtime.name}' has no supportedModelFormats")
5490

5591
with create_isvc(
5692
client=admin_client,
5793
name="negative-test-ovms-isvc",
58-
namespace=unprivileged_model_namespace.name,
94+
namespace=negative_test_namespace.name,
5995
runtime=ovms_serving_runtime.name,
60-
storage_key=ci_endpoint_s3_secret.name,
96+
storage_key=negative_test_s3_secret.name,
6197
storage_path=urlparse(storage_uri).path,
6298
model_format=supported_formats[0].name,
6399
deployment_mode=KServeDeploymentType.RAW_DEPLOYMENT,
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Tests for invalid model name in inference endpoint.
2+
3+
Jira: RHOAIENG-48282
4+
"""
5+
6+
import json
7+
from http import HTTPStatus
8+
from typing import Any
9+
10+
import pytest
11+
from kubernetes.dynamic import DynamicClient
12+
from ocp_resources.inference_service import InferenceService
13+
14+
from tests.model_serving.model_server.kserve.negative.utils import (
15+
VALID_OVMS_INFERENCE_BODY,
16+
assert_pods_healthy,
17+
send_inference_request,
18+
)
19+
20+
pytestmark = pytest.mark.usefixtures("valid_aws_config")
21+
22+
VALID_BODY_RAW = json.dumps(VALID_OVMS_INFERENCE_BODY)
23+
24+
25+
@pytest.mark.tier1
26+
class TestInvalidModelName:
27+
"""Test class for verifying error handling when targeting a non-existent model.
28+
29+
Preconditions:
30+
- InferenceService "negative-test-ovms-isvc" deployed and ready
31+
- No InferenceService with name "nonexistent-model"
32+
33+
Test Steps:
34+
1. Create InferenceService with OVMS runtime
35+
2. Wait for InferenceService status = Ready
36+
3. Send inference request to /v2/models/nonexistent-model/infer
37+
4. Verify error response and existing service health
38+
39+
Expected Results:
40+
- HTTP Status Code: 404 Not Found
41+
- Error message indicates model not found
42+
- No impact on existing model service
43+
"""
44+
45+
def test_nonexistent_model_returns_404(
46+
self,
47+
negative_test_ovms_isvc: InferenceService,
48+
) -> None:
49+
"""Verify that inference to a non-existent model returns 404 status code.
50+
51+
Given an InferenceService is deployed and ready
52+
When sending a POST request targeting a non-existent model name
53+
Then the response should have HTTP status code 404 (Not Found)
54+
"""
55+
status_code, response_body = send_inference_request(
56+
inference_service=negative_test_ovms_isvc,
57+
body=VALID_BODY_RAW,
58+
model_name="nonexistent-model",
59+
)
60+
61+
assert status_code == HTTPStatus.NOT_FOUND, (
62+
f"Expected 404 Not Found for nonexistent model, got {status_code}. Response: {response_body}"
63+
)
64+
65+
def test_existing_service_unaffected_after_invalid_model_request(
66+
self,
67+
admin_client: DynamicClient,
68+
negative_test_ovms_isvc: InferenceService,
69+
initial_pod_state: dict[str, dict[str, Any]],
70+
) -> None:
71+
"""Verify that the existing service remains healthy after invalid model requests.
72+
73+
Given an InferenceService is deployed and ready
74+
When sending a request targeting a non-existent model name
75+
Then the existing service pods should remain running without restarts
76+
"""
77+
send_inference_request(
78+
inference_service=negative_test_ovms_isvc,
79+
body=VALID_BODY_RAW,
80+
model_name="nonexistent-model",
81+
)
82+
assert_pods_healthy(
83+
admin_client=admin_client,
84+
isvc=negative_test_ovms_isvc,
85+
initial_pod_state=initial_pod_state,
86+
)
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""Tests for malformed JSON payload handling in inference requests.
2+
3+
Jira: RHOAIENG-48279
4+
"""
5+
6+
from http import HTTPStatus
7+
from typing import Any
8+
9+
import pytest
10+
from kubernetes.dynamic import DynamicClient
11+
from ocp_resources.inference_service import InferenceService
12+
13+
from tests.model_serving.model_server.kserve.negative.utils import (
14+
assert_pods_healthy,
15+
send_inference_request,
16+
)
17+
18+
pytestmark = pytest.mark.usefixtures("valid_aws_config")
19+
20+
MALFORMED_JSON_EXPECTED_CODES: set[int] = {
21+
HTTPStatus.BAD_REQUEST,
22+
HTTPStatus.PRECONDITION_FAILED,
23+
}
24+
MISSING_BRACE_BODY = '{"inputs": [{"name": "Input3"'
25+
TRAILING_COMMA_BODY = '{"inputs": [{"name": "Input3",}]}'
26+
27+
28+
@pytest.mark.tier1
29+
@pytest.mark.rawdeployment
30+
class TestMalformedJsonPayload:
31+
"""Test class for verifying error handling when receiving malformed JSON payloads.
32+
33+
Preconditions:
34+
- InferenceService deployed with OVMS runtime (RawDeployment)
35+
- Model is ready and serving
36+
37+
Test Steps:
38+
1. Create InferenceService with OVMS runtime
39+
2. Wait for InferenceService status = Ready
40+
3. Send POST with malformed JSON bodies (missing brace, trailing comma, plain text)
41+
4. Verify error responses and pod health
42+
43+
Expected Results:
44+
- HTTP Status Code: 400 Bad Request or 412 Precondition Failed
45+
(OVMS returns 412 for JSON parse errors)
46+
- Response indicates JSON parse failure
47+
- No pod crash or restart
48+
"""
49+
50+
@pytest.mark.parametrize(
51+
"malformed_body",
52+
[
53+
pytest.param(MISSING_BRACE_BODY, id="missing_closing_brace"),
54+
pytest.param(TRAILING_COMMA_BODY, id="trailing_comma"),
55+
pytest.param("not json at all", id="plain_text"),
56+
],
57+
)
58+
def test_malformed_json_returns_error(
59+
self,
60+
negative_test_ovms_isvc: InferenceService,
61+
malformed_body: str,
62+
) -> None:
63+
"""Verify that malformed JSON payloads return an error status code.
64+
65+
Given an InferenceService is deployed and ready
66+
When sending a POST request with a malformed JSON body
67+
Then the response should have HTTP status code 400 or 412
68+
"""
69+
status_code, response_body = send_inference_request(
70+
inference_service=negative_test_ovms_isvc,
71+
body=malformed_body,
72+
)
73+
74+
assert status_code in MALFORMED_JSON_EXPECTED_CODES, (
75+
f"Expected 400 or 412 for malformed JSON, got {status_code}. Response: {response_body}"
76+
)
77+
78+
def test_model_pod_remains_healthy_after_malformed_json(
79+
self,
80+
admin_client: DynamicClient,
81+
negative_test_ovms_isvc: InferenceService,
82+
initial_pod_state: dict[str, dict[str, Any]],
83+
) -> None:
84+
"""Verify that the model pod remains healthy after receiving malformed JSON.
85+
86+
Given an InferenceService is deployed and ready
87+
When sending requests with malformed JSON payloads
88+
Then the same pods should still be running without additional restarts
89+
"""
90+
send_inference_request(
91+
inference_service=negative_test_ovms_isvc,
92+
body=MISSING_BRACE_BODY,
93+
)
94+
assert_pods_healthy(
95+
admin_client=admin_client,
96+
isvc=negative_test_ovms_isvc,
97+
initial_pod_state=initial_pod_state,
98+
)

0 commit comments

Comments
 (0)