Skip to content

Commit 2fc3042

Browse files
authored
Merge branch 'main' into mcp_load
2 parents 2d19553 + 44389d6 commit 2fc3042

11 files changed

Lines changed: 917 additions & 99 deletions

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+
)
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""Tests for missing required fields in inference requests.
2+
3+
Jira: RHOAIENG-48281
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+
assert_pods_healthy,
16+
send_inference_request,
17+
)
18+
19+
pytestmark = pytest.mark.usefixtures("valid_aws_config")
20+
21+
22+
@pytest.mark.tier1
23+
@pytest.mark.rawdeployment
24+
class TestMissingRequiredFields:
25+
"""Test class for verifying error handling when required fields are missing.
26+
27+
Preconditions:
28+
- InferenceService deployed with OVMS runtime
29+
- Model is ready and serving
30+
31+
Test Steps:
32+
1. Create InferenceService with OVMS runtime
33+
2. Wait for InferenceService status = Ready
34+
3. Send POST with empty body {}
35+
4. Send POST with body missing "inputs" field
36+
5. Verify error responses and pod health
37+
38+
Expected Results:
39+
- HTTP Status Code: 400 Bad Request
40+
- Error message indicates missing required field
41+
- No server crash
42+
"""
43+
44+
@pytest.mark.parametrize(
45+
"incomplete_body",
46+
[
47+
pytest.param("{}", id="empty_body"),
48+
pytest.param(json.dumps({"id": "test-123"}), id="missing_inputs_field"),
49+
],
50+
)
51+
def test_missing_required_fields_returns_400(
52+
self,
53+
negative_test_ovms_isvc: InferenceService,
54+
incomplete_body: str,
55+
) -> None:
56+
"""Verify that requests missing required fields return 400 status code.
57+
58+
Given an InferenceService is deployed and ready
59+
When sending a POST request with missing required fields
60+
Then the response should have HTTP status code 400 (Bad Request)
61+
"""
62+
status_code, response_body = send_inference_request(
63+
inference_service=negative_test_ovms_isvc,
64+
body=incomplete_body,
65+
)
66+
67+
assert status_code == HTTPStatus.BAD_REQUEST, (
68+
f"Expected 400 Bad Request for incomplete payload, got {status_code}. Response: {response_body}"
69+
)
70+
71+
def test_model_pod_remains_healthy_after_missing_fields(
72+
self,
73+
admin_client: DynamicClient,
74+
negative_test_ovms_isvc: InferenceService,
75+
initial_pod_state: dict[str, dict[str, Any]],
76+
) -> None:
77+
"""Verify that the model pod remains healthy after receiving incomplete requests.
78+
79+
Given an InferenceService is deployed and ready
80+
When sending requests with missing required fields
81+
Then the same pods should still be running without additional restarts
82+
"""
83+
send_inference_request(
84+
inference_service=negative_test_ovms_isvc,
85+
body="{}",
86+
)
87+
assert_pods_healthy(
88+
admin_client=admin_client,
89+
isvc=negative_test_ovms_isvc,
90+
initial_pod_state=initial_pod_state,
91+
)

0 commit comments

Comments
 (0)