mirror of
https://github.com/thousandeyes/thousandeyes-sdk-python.git
synced 2026-08-04 08:56:50 +00:00
Some checks failed
Python CI / build (push) Has been cancelled
* CP-2453 Add core test mock server and exception status mapping Move the integration mock server into test-only sdk_test_support helpers and expose ApiException.exception_class_for_http_status for generated integration error assertions. Co-authored-by: Cursor <cursoragent@cursor.com> * add test for validating OAS not match case --------- Co-authored-by: Cursor <cursoragent@cursor.com>
316 lines
9.8 KiB
Python
316 lines
9.8 KiB
Python
import json
|
|
|
|
import pytest
|
|
|
|
from sdk_test_support.mock_server import (
|
|
AUTHORIZATION_HEADER,
|
|
ERROR_STATUS_HEADER,
|
|
OPERATION_ID_HEADER,
|
|
MockApiServer,
|
|
)
|
|
from sdk_test_support.mock_server_types import ErrorResponseExpectation, OperationExpectation
|
|
|
|
|
|
@pytest.fixture
|
|
def manifest():
|
|
return {
|
|
"createAlertRule": OperationExpectation(
|
|
operation_id="createAlertRule",
|
|
method="POST",
|
|
path="/alerts/rules",
|
|
request_body_example={"ruleName": "Example"},
|
|
success_status=201,
|
|
success_body={"ruleId": "1"},
|
|
error_responses={
|
|
"400": ErrorResponseExpectation(
|
|
status=400,
|
|
body={"title": "Bad Request", "status": 400},
|
|
)
|
|
},
|
|
),
|
|
"deleteAlertRule": OperationExpectation(
|
|
operation_id="deleteAlertRule",
|
|
method="DELETE",
|
|
path="/alerts/rules/{ruleId}",
|
|
path_param_examples={"ruleId": "127094"},
|
|
success_status=204,
|
|
success_body=None,
|
|
),
|
|
"getAlertRule": OperationExpectation(
|
|
operation_id="getAlertRule",
|
|
method="GET",
|
|
path="/alerts/rules/{ruleId}",
|
|
path_param_examples={"ruleId": "127094"},
|
|
success_status=200,
|
|
success_body={"ruleId": "127094", "ruleName": "Example"},
|
|
),
|
|
}
|
|
|
|
|
|
def _request(server: MockApiServer, *, method: str, path: str, headers=None, body=None):
|
|
import urllib.request
|
|
|
|
request_headers = {
|
|
AUTHORIZATION_HEADER: "Bearer test-token",
|
|
OPERATION_ID_HEADER: "createAlertRule",
|
|
}
|
|
if headers:
|
|
request_headers.update(headers)
|
|
|
|
data = None
|
|
if body is not None:
|
|
data = json.dumps(body).encode("utf-8")
|
|
request_headers["Content-Type"] = "application/json"
|
|
|
|
request = urllib.request.Request(
|
|
server.base_url + path,
|
|
data=data,
|
|
headers=request_headers,
|
|
method=method,
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(request) as response:
|
|
return response.status, response.read()
|
|
except urllib.error.HTTPError as exc:
|
|
return exc.code, exc.read()
|
|
|
|
|
|
def test_mock_server_happy_path(manifest):
|
|
with MockApiServer(manifest) as server:
|
|
status, body = _request(
|
|
server,
|
|
method="POST",
|
|
path="/alerts/rules",
|
|
body={"ruleName": "Example"},
|
|
)
|
|
assert status == 201
|
|
assert json.loads(body.decode("utf-8")) == {"ruleId": "1"}
|
|
|
|
|
|
def test_mock_server_rejects_missing_authorization(manifest):
|
|
with MockApiServer(manifest) as server:
|
|
status, _ = _request(
|
|
server,
|
|
method="POST",
|
|
path="/alerts/rules",
|
|
headers={AUTHORIZATION_HEADER: ""},
|
|
body={"ruleName": "Example"},
|
|
)
|
|
assert status == 401
|
|
|
|
|
|
def test_mock_server_rejects_invalid_request_body(manifest):
|
|
with MockApiServer(manifest) as server:
|
|
status, _ = _request(
|
|
server,
|
|
method="POST",
|
|
path="/alerts/rules",
|
|
body={"ruleName": "Wrong"},
|
|
)
|
|
assert status == 400
|
|
|
|
|
|
def test_mock_server_error_path(manifest):
|
|
with MockApiServer(manifest) as server:
|
|
status, body = _request(
|
|
server,
|
|
method="POST",
|
|
path="/alerts/rules",
|
|
headers={ERROR_STATUS_HEADER: "400"},
|
|
body={"unexpected": True},
|
|
)
|
|
assert status == 400
|
|
assert json.loads(body.decode("utf-8"))["title"] == "Bad Request"
|
|
|
|
|
|
def test_mock_server_ignores_readonly_fields_in_expected_body(manifest):
|
|
readonly_manifest = {
|
|
**manifest,
|
|
"createAlertRule": OperationExpectation(
|
|
operation_id="createAlertRule",
|
|
method="POST",
|
|
path="/alerts/rules",
|
|
request_body_example={"ruleName": "Example", "ruleId": "read-only"},
|
|
success_status=201,
|
|
success_body={"ruleId": "1"},
|
|
),
|
|
}
|
|
with MockApiServer(readonly_manifest) as server:
|
|
status, body = _request(
|
|
server,
|
|
method="POST",
|
|
path="/alerts/rules",
|
|
body={"ruleName": "Example"},
|
|
)
|
|
assert status == 201
|
|
assert json.loads(body.decode("utf-8")) == {"ruleId": "1"}
|
|
|
|
|
|
def test_mock_server_no_content_response(manifest):
|
|
with MockApiServer(manifest) as server:
|
|
import urllib.request
|
|
|
|
request = urllib.request.Request(
|
|
server.base_url + "/alerts/rules/127094",
|
|
headers={
|
|
AUTHORIZATION_HEADER: "Bearer test-token",
|
|
OPERATION_ID_HEADER: "deleteAlertRule",
|
|
},
|
|
method="DELETE",
|
|
)
|
|
with urllib.request.urlopen(request) as response:
|
|
assert response.status == 204
|
|
assert response.read() == b""
|
|
|
|
|
|
def test_mock_server_matches_path_variable(manifest):
|
|
with MockApiServer(manifest) as server:
|
|
status, body = _request(
|
|
server,
|
|
method="GET",
|
|
path="/alerts/rules/127094",
|
|
headers={OPERATION_ID_HEADER: "getAlertRule"},
|
|
)
|
|
assert status == 200
|
|
assert json.loads(body.decode("utf-8")) == {"ruleId": "127094", "ruleName": "Example"}
|
|
|
|
|
|
def test_mock_server_rejects_path_missing_path_variable(manifest):
|
|
with MockApiServer(manifest) as server:
|
|
status, body = _request(
|
|
server,
|
|
method="GET",
|
|
path="/alerts/rules",
|
|
headers={OPERATION_ID_HEADER: "getAlertRule"},
|
|
)
|
|
assert status == 400
|
|
assert json.loads(body.decode("utf-8"))["detail"] == "Path does not match operation expectation"
|
|
|
|
|
|
def test_mock_server_accepts_equivalent_iso8601_datetime_formats(manifest):
|
|
datetime_manifest = {
|
|
**manifest,
|
|
"createAlertRule": OperationExpectation(
|
|
operation_id="createAlertRule",
|
|
method="POST",
|
|
path="/alerts/rules",
|
|
request_body_example={
|
|
"ruleName": "Example",
|
|
"startDate": "2017-07-01T05:00:00Z",
|
|
},
|
|
success_status=201,
|
|
success_body={"ruleId": "1"},
|
|
),
|
|
}
|
|
with MockApiServer(datetime_manifest) as server:
|
|
status, body = _request(
|
|
server,
|
|
method="POST",
|
|
path="/alerts/rules",
|
|
body={
|
|
"ruleName": "Example",
|
|
"startDate": "2017-07-01T05:00:00+00:00",
|
|
},
|
|
)
|
|
assert status == 201
|
|
assert json.loads(body.decode("utf-8")) == {"ruleId": "1"}
|
|
|
|
|
|
def test_mock_server_ignores_readonly_fields_in_nested_request_objects(manifest):
|
|
nested_manifest = {
|
|
**manifest,
|
|
"createAlertRule": OperationExpectation(
|
|
operation_id="createAlertRule",
|
|
method="POST",
|
|
path="/alerts/rules",
|
|
request_body_example={
|
|
"ruleName": "Example",
|
|
"widgets": [
|
|
{
|
|
"title": "Widget Title",
|
|
"id": "read-only-id",
|
|
"embedUrl": "https://example.com/embed",
|
|
}
|
|
],
|
|
},
|
|
success_status=201,
|
|
success_body={"ruleId": "1"},
|
|
),
|
|
}
|
|
with MockApiServer(nested_manifest) as server:
|
|
status, body = _request(
|
|
server,
|
|
method="POST",
|
|
path="/alerts/rules",
|
|
body={
|
|
"ruleName": "Example",
|
|
"widgets": [{"title": "Widget Title"}],
|
|
},
|
|
)
|
|
assert status == 201
|
|
assert json.loads(body.decode("utf-8")) == {"ruleId": "1"}
|
|
|
|
|
|
def test_integration_error_assertion_fails_when_deserialized_error_does_not_match_oas_example():
|
|
"""Generated error-path tests compare ApiException.data to the OAS error example."""
|
|
import unittest
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
from pydantic import BaseModel, ConfigDict
|
|
|
|
class Error(BaseModel):
|
|
title: str
|
|
status: int
|
|
|
|
model_config = ConfigDict(extra="allow")
|
|
|
|
def to_json(self) -> str:
|
|
return self.model_dump_json()
|
|
|
|
mismatched_manifest = {
|
|
"createAlertRule": OperationExpectation(
|
|
operation_id="createAlertRule",
|
|
method="POST",
|
|
path="/alerts/rules",
|
|
request_body_example={"ruleName": "Example"},
|
|
success_status=201,
|
|
success_body={"ruleId": "1"},
|
|
error_responses={
|
|
"400": ErrorResponseExpectation(
|
|
status=400,
|
|
body={"title": "Wrong Title", "status": 400},
|
|
)
|
|
},
|
|
),
|
|
}
|
|
oas_example = {"title": "Bad Request", "status": 400}
|
|
|
|
with MockApiServer(mismatched_manifest) as server:
|
|
request = urllib.request.Request(
|
|
server.base_url + "/alerts/rules",
|
|
data=json.dumps({"unexpected": True}).encode("utf-8"),
|
|
headers={
|
|
AUTHORIZATION_HEADER: "Bearer test-token",
|
|
OPERATION_ID_HEADER: "createAlertRule",
|
|
ERROR_STATUS_HEADER: "400",
|
|
"Content-Type": "application/json",
|
|
},
|
|
method="POST",
|
|
)
|
|
with pytest.raises(urllib.error.HTTPError) as http_error:
|
|
urllib.request.urlopen(request)
|
|
wire_body = json.loads(http_error.value.read().decode("utf-8"))
|
|
|
|
exception_data = Error.model_validate(wire_body)
|
|
|
|
def assert_constructed_model_matches_example_json(model, loaded_json):
|
|
test_case = unittest.TestCase()
|
|
test_case.assertEqual(
|
|
json.dumps(loaded_json, sort_keys=True),
|
|
json.dumps(json.loads(model.to_json()), sort_keys=True),
|
|
)
|
|
|
|
with pytest.raises(AssertionError):
|
|
assert_constructed_model_matches_example_json(exception_data, oas_example)
|