This asserted that _redact_sensitive recurses into a dict under a
sensitive key — redacting auth.token while leaving auth.type readable.
The source replaces the whole value the moment the KEY matches, so
redacted["config"]["auth"] is a string and indexing ["token"] into it
raises TypeError. Source and test arrived in the same commit, so this
was never drift: it was a disagreement nobody settled.
Settled in favour of the source. Fine-grained redaction has to know
which sub-keys carry a secret, which is a guess about the shape of data
nobody has inspected; matching on the key cannot be wrong that way. Real
configs here are {"auth": {"type": "bearer", "token": "${SOME_KEY}"}},
and the cost of guessing wrong is a credential in a log, which no later
fix undoes. The price is readability, and it is paid deliberately.
Adds a second assertion that the secret appears nowhere in the output by
any path, which is the property actually worth protecting.
Co-Authored-By: Claude <noreply@anthropic.com>
652 lines
22 KiB
Python
652 lines
22 KiB
Python
"""
|
|
Comprehensive tests for the generic REST API executor.
|
|
|
|
Tests cover:
|
|
- All HTTP methods (GET, POST, PUT, DELETE, PATCH)
|
|
- Authentication types (Bearer, Basic, API Key)
|
|
- Environment variable substitution
|
|
- Error handling
|
|
- Response extraction
|
|
- Sensitive data redaction
|
|
|
|
Run with: pytest tests/test_rest_api_executor.py -v -s
|
|
"""
|
|
import pytest
|
|
import asyncio
|
|
import os
|
|
from unittest.mock import AsyncMock, patch, MagicMock
|
|
import httpx
|
|
|
|
from src.executors import rest_api_executor
|
|
from src.config import Settings
|
|
|
|
|
|
@pytest.mark.executor
|
|
@pytest.mark.unit
|
|
class TestRestApiExecutor:
|
|
"""Tests for rest_api_executor module."""
|
|
|
|
@pytest.fixture
|
|
def mock_httpx_client(self):
|
|
"""Mock httpx AsyncClient."""
|
|
return AsyncMock()
|
|
|
|
@pytest.fixture
|
|
def base_config(self):
|
|
"""Base configuration for REST API calls."""
|
|
return {
|
|
"url": "http://test-service:8080/api/endpoint",
|
|
"method": "POST"
|
|
}
|
|
|
|
# Basic HTTP Method Tests
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_post_request_success(self, test_settings: Settings):
|
|
"""Test successful POST request."""
|
|
config = {
|
|
"url": "http://httpbin.org/post",
|
|
"method": "POST",
|
|
"payload": {"test": "data"},
|
|
"timeout": 10
|
|
}
|
|
|
|
# Mock httpx response
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"message": "Success"}
|
|
mock_response.text = "Success"
|
|
|
|
mock_client.return_value.__aenter__.return_value.post = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
result = await rest_api_executor.execute(config, test_settings)
|
|
|
|
assert "Success" in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_request_success(self, test_settings: Settings):
|
|
"""Test successful GET request."""
|
|
config = {
|
|
"url": "http://httpbin.org/get",
|
|
"method": "GET",
|
|
"timeout": 10
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"result": "GET success"}
|
|
mock_response.text = "Success"
|
|
|
|
mock_client.return_value.__aenter__.return_value.get = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
result = await rest_api_executor.execute(config, test_settings)
|
|
|
|
assert result is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_put_request_success(self, test_settings: Settings):
|
|
"""Test successful PUT request."""
|
|
config = {
|
|
"url": "http://httpbin.org/put",
|
|
"method": "PUT",
|
|
"payload": {"update": "data"}
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"message": "Updated"}
|
|
mock_response.text = "Updated"
|
|
|
|
mock_client.return_value.__aenter__.return_value.put = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
result = await rest_api_executor.execute(config, test_settings)
|
|
|
|
assert "Updated" in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_request_success(self, test_settings: Settings):
|
|
"""Test successful DELETE request."""
|
|
config = {
|
|
"url": "http://httpbin.org/delete",
|
|
"method": "DELETE"
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 204
|
|
mock_response.json.side_effect = Exception("No JSON")
|
|
mock_response.text = ""
|
|
|
|
mock_client.return_value.__aenter__.return_value.delete = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
result = await rest_api_executor.execute(config, test_settings)
|
|
|
|
assert "204" in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_patch_request_success(self, test_settings: Settings):
|
|
"""Test successful PATCH request."""
|
|
config = {
|
|
"url": "http://httpbin.org/patch",
|
|
"method": "PATCH",
|
|
"payload": {"field": "value"}
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"message": "Patched"}
|
|
mock_response.text = "Patched"
|
|
|
|
mock_client.return_value.__aenter__.return_value.patch = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
result = await rest_api_executor.execute(config, test_settings)
|
|
|
|
assert "Patched" in result
|
|
|
|
# Authentication Tests
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bearer_auth(self, test_settings: Settings):
|
|
"""Test Bearer token authentication."""
|
|
config = {
|
|
"url": "http://test-service/api",
|
|
"method": "GET",
|
|
"auth": {
|
|
"type": "bearer",
|
|
"token": "test-token-123"
|
|
}
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"message": "Authenticated"}
|
|
mock_response.text = "Authenticated"
|
|
|
|
mock_get = AsyncMock(return_value=mock_response)
|
|
mock_client.return_value.__aenter__.return_value.get = mock_get
|
|
|
|
result = await rest_api_executor.execute(config, test_settings)
|
|
|
|
# Verify Authorization header was set
|
|
call_args = mock_get.call_args
|
|
headers = call_args.kwargs.get('headers', {})
|
|
assert 'Authorization' in headers
|
|
assert headers['Authorization'] == 'Bearer test-token-123'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_basic_auth(self, test_settings: Settings):
|
|
"""Test Basic authentication."""
|
|
config = {
|
|
"url": "http://test-service/api",
|
|
"method": "GET",
|
|
"auth": {
|
|
"type": "basic",
|
|
"username": "testuser",
|
|
"password": "testpass"
|
|
}
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"message": "Authenticated"}
|
|
mock_response.text = "Authenticated"
|
|
|
|
mock_get = AsyncMock(return_value=mock_response)
|
|
mock_client.return_value.__aenter__.return_value.get = mock_get
|
|
|
|
result = await rest_api_executor.execute(config, test_settings)
|
|
|
|
# Verify Authorization header was set
|
|
call_args = mock_get.call_args
|
|
headers = call_args.kwargs.get('headers', {})
|
|
assert 'Authorization' in headers
|
|
assert headers['Authorization'].startswith('Basic ')
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_api_key_auth(self, test_settings: Settings):
|
|
"""Test API Key authentication."""
|
|
config = {
|
|
"url": "http://test-service/api",
|
|
"method": "GET",
|
|
"auth": {
|
|
"type": "api_key",
|
|
"key": "my-api-key-123",
|
|
"header": "X-API-Key"
|
|
}
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"message": "Authenticated"}
|
|
mock_response.text = "Authenticated"
|
|
|
|
mock_get = AsyncMock(return_value=mock_response)
|
|
mock_client.return_value.__aenter__.return_value.get = mock_get
|
|
|
|
result = await rest_api_executor.execute(config, test_settings)
|
|
|
|
# Verify API key header was set
|
|
call_args = mock_get.call_args
|
|
headers = call_args.kwargs.get('headers', {})
|
|
assert 'X-API-Key' in headers
|
|
assert headers['X-API-Key'] == 'my-api-key-123'
|
|
|
|
# Environment Variable Substitution Tests
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_env_var_substitution_in_url(self, test_settings: Settings):
|
|
"""Test environment variable substitution in URL."""
|
|
os.environ['TEST_SERVICE_URL'] = 'http://my-service:8080'
|
|
|
|
config = {
|
|
"url": "${TEST_SERVICE_URL}/api/endpoint",
|
|
"method": "GET"
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"message": "Success"}
|
|
mock_response.text = "Success"
|
|
|
|
mock_get = AsyncMock(return_value=mock_response)
|
|
mock_client.return_value.__aenter__.return_value.get = mock_get
|
|
|
|
result = await rest_api_executor.execute(config, test_settings)
|
|
|
|
# Verify URL was substituted
|
|
call_args = mock_get.call_args
|
|
url = call_args.args[0] if call_args.args else None
|
|
assert url == 'http://my-service:8080/api/endpoint'
|
|
|
|
del os.environ['TEST_SERVICE_URL']
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_env_var_substitution_in_auth_token(self, test_settings: Settings):
|
|
"""Test environment variable substitution in auth token."""
|
|
os.environ['API_TOKEN'] = 'secret-token-from-env'
|
|
|
|
config = {
|
|
"url": "http://test-service/api",
|
|
"method": "GET",
|
|
"auth": {
|
|
"type": "bearer",
|
|
"token": "${API_TOKEN}"
|
|
}
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {}
|
|
mock_response.text = "Success"
|
|
|
|
mock_get = AsyncMock(return_value=mock_response)
|
|
mock_client.return_value.__aenter__.return_value.get = mock_get
|
|
|
|
result = await rest_api_executor.execute(config, test_settings)
|
|
|
|
# Verify token was substituted
|
|
call_args = mock_get.call_args
|
|
headers = call_args.kwargs.get('headers', {})
|
|
assert headers['Authorization'] == 'Bearer secret-token-from-env'
|
|
|
|
del os.environ['API_TOKEN']
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_env_var_substitution_in_payload(self, test_settings: Settings):
|
|
"""Test environment variable substitution in payload."""
|
|
os.environ['DATABASE_NAME'] = 'test_db'
|
|
|
|
config = {
|
|
"url": "http://test-service/api",
|
|
"method": "POST",
|
|
"payload": {
|
|
"database": "${DATABASE_NAME}",
|
|
"action": "backup"
|
|
}
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {}
|
|
mock_response.text = "Success"
|
|
|
|
mock_post = AsyncMock(return_value=mock_response)
|
|
mock_client.return_value.__aenter__.return_value.post = mock_post
|
|
|
|
result = await rest_api_executor.execute(config, test_settings)
|
|
|
|
# Verify payload was substituted
|
|
call_args = mock_post.call_args
|
|
payload = call_args.kwargs.get('json', {})
|
|
assert payload['database'] == 'test_db'
|
|
|
|
del os.environ['DATABASE_NAME']
|
|
|
|
# Response Extraction Tests
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_response_path_extraction(self, test_settings: Settings):
|
|
"""Test extracting specific field from response."""
|
|
config = {
|
|
"url": "http://test-service/api",
|
|
"method": "GET",
|
|
"response_path": "result.message"
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"result": {
|
|
"message": "Extracted message",
|
|
"other": "ignored"
|
|
}
|
|
}
|
|
mock_response.text = "Success"
|
|
|
|
mock_client.return_value.__aenter__.return_value.get = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
result = await rest_api_executor.execute(config, test_settings)
|
|
|
|
assert result == "Extracted message"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_response_path_not_found(self, test_settings: Settings):
|
|
"""Test response path extraction when field doesn't exist."""
|
|
config = {
|
|
"url": "http://test-service/api",
|
|
"method": "GET",
|
|
"response_path": "result.missing"
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"result": {"other": "data"}}
|
|
mock_response.text = "Success"
|
|
|
|
mock_client.return_value.__aenter__.return_value.get = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
result = await rest_api_executor.execute(config, test_settings)
|
|
|
|
# Should fallback to default message
|
|
assert result is not None
|
|
|
|
# Error Handling Tests
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_missing_url_raises_error(self, test_settings: Settings):
|
|
"""Test that missing URL raises ValueError."""
|
|
config = {
|
|
"method": "GET"
|
|
}
|
|
|
|
with pytest.raises(ValueError, match="Missing required config: 'url'"):
|
|
await rest_api_executor.execute(config, test_settings)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_invalid_http_method_raises_error(self, test_settings: Settings):
|
|
"""Test that invalid HTTP method raises ValueError."""
|
|
config = {
|
|
"url": "http://test-service/api",
|
|
"method": "INVALID"
|
|
}
|
|
|
|
with pytest.raises(ValueError, match="Invalid HTTP method"):
|
|
await rest_api_executor.execute(config, test_settings)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_http_error_status_code(self, test_settings: Settings):
|
|
"""Test handling of HTTP error status codes."""
|
|
config = {
|
|
"url": "http://test-service/api",
|
|
"method": "GET"
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 500
|
|
mock_response.text = "Internal Server Error"
|
|
|
|
mock_client.return_value.__aenter__.return_value.get = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
with pytest.raises(Exception, match="API call failed with status 500"):
|
|
await rest_api_executor.execute(config, test_settings)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_custom_success_codes(self, test_settings: Settings):
|
|
"""Test custom success codes configuration."""
|
|
config = {
|
|
"url": "http://test-service/api",
|
|
"method": "POST",
|
|
"success_codes": [201, 202]
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 201
|
|
mock_response.json.return_value = {"message": "Created"}
|
|
mock_response.text = "Created"
|
|
|
|
mock_client.return_value.__aenter__.return_value.post = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
result = await rest_api_executor.execute(config, test_settings)
|
|
|
|
assert "Created" in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_network_error_handling(self, test_settings: Settings):
|
|
"""Test handling of network errors."""
|
|
config = {
|
|
"url": "http://unreachable-service/api",
|
|
"method": "GET",
|
|
"timeout": 1
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_client.return_value.__aenter__.return_value.get = AsyncMock(
|
|
side_effect=httpx.ConnectError("Connection refused")
|
|
)
|
|
|
|
with pytest.raises(Exception, match="Request failed"):
|
|
await rest_api_executor.execute(config, test_settings)
|
|
|
|
# Sensitive Data Redaction Tests
|
|
|
|
def test_redact_sensitive_data_in_dict(self):
|
|
"""Test redaction of sensitive fields in dictionaries."""
|
|
data = {
|
|
"username": "user",
|
|
"password": "secret123",
|
|
"api_key": "key123",
|
|
"normal_field": "visible"
|
|
}
|
|
|
|
redacted = rest_api_executor._redact_sensitive(data)
|
|
|
|
assert redacted["username"] == "user"
|
|
assert redacted["password"] == "***REDACTED***"
|
|
assert redacted["api_key"] == "***REDACTED***"
|
|
assert redacted["normal_field"] == "visible"
|
|
|
|
def test_redact_sensitive_nested(self):
|
|
"""A dict under a sensitive key is redacted whole, not recursed into.
|
|
|
|
This asserted fine-grained recursion — that `auth.token` was replaced
|
|
while `auth`'s other keys stayed readable — and had never passed. The
|
|
source redacts the entire value the moment the KEY matches, so
|
|
`redacted["config"]["auth"]` is the string, and indexing `["token"]`
|
|
into it raises TypeError.
|
|
|
|
Settled in favour of the source. Fine-grained redaction has to know
|
|
which sub-keys carry the secret, which is a guess about the shape of
|
|
data nobody has inspected; redacting on the key cannot be wrong that
|
|
way. Real configs here look like
|
|
{"auth": {"type": "bearer", "token": "${SOME_API_KEY}"}}, and the cost
|
|
of guessing wrong is a credential in a log, which no later fix undoes.
|
|
|
|
The price is readability: a reader learns that auth was present, not
|
|
that it was bearer. That is the trade being made deliberately.
|
|
"""
|
|
data = {
|
|
"config": {
|
|
"database": "mydb",
|
|
"password": "secret",
|
|
"auth": {
|
|
"token": "bearer123"
|
|
}
|
|
}
|
|
}
|
|
|
|
redacted = rest_api_executor._redact_sensitive(data)
|
|
|
|
assert redacted["config"]["database"] == "mydb"
|
|
assert redacted["config"]["password"] == "***REDACTED***"
|
|
# The whole sub-dict, not a recursed copy of it.
|
|
assert redacted["config"]["auth"] == "***REDACTED***"
|
|
# And the secret is nowhere in the output, by any path.
|
|
assert "bearer123" not in str(redacted)
|
|
|
|
# Helper Function Tests
|
|
|
|
def test_extract_json_path_simple(self):
|
|
"""Test simple JSONPath extraction."""
|
|
data = {"message": "test"}
|
|
result = rest_api_executor._extract_json_path(data, "message")
|
|
assert result == "test"
|
|
|
|
def test_extract_json_path_nested(self):
|
|
"""Test nested JSONPath extraction."""
|
|
data = {"result": {"status": {"message": "success"}}}
|
|
result = rest_api_executor._extract_json_path(data, "result.status.message")
|
|
assert result == "success"
|
|
|
|
def test_extract_json_path_not_found(self):
|
|
"""Test JSONPath extraction when path doesn't exist."""
|
|
data = {"message": "test"}
|
|
result = rest_api_executor._extract_json_path(data, "missing.path")
|
|
assert result is None
|
|
|
|
def test_substitute_env_vars(self):
|
|
"""Test environment variable substitution."""
|
|
os.environ['TEST_VAR'] = 'test_value'
|
|
|
|
result = rest_api_executor._substitute_env_vars("Prefix ${TEST_VAR} suffix")
|
|
assert result == "Prefix test_value suffix"
|
|
|
|
del os.environ['TEST_VAR']
|
|
|
|
def test_substitute_env_vars_missing(self):
|
|
"""Test substitution with missing environment variable."""
|
|
result = rest_api_executor._substitute_env_vars("Prefix ${MISSING_VAR} suffix")
|
|
# Should replace with empty string
|
|
assert result == "Prefix suffix"
|
|
|
|
def test_substitute_env_vars_recursive(self):
|
|
"""Test recursive environment variable substitution."""
|
|
os.environ['HOST'] = 'localhost'
|
|
os.environ['PORT'] = '8080'
|
|
|
|
data = {
|
|
"url": "http://${HOST}:${PORT}/api",
|
|
"nested": {
|
|
"key": "${HOST}"
|
|
}
|
|
}
|
|
|
|
result = rest_api_executor._substitute_env_vars_recursive(data)
|
|
|
|
assert result["url"] == "http://localhost:8080/api"
|
|
assert result["nested"]["key"] == "localhost"
|
|
|
|
del os.environ['HOST']
|
|
del os.environ['PORT']
|
|
|
|
# Configuration Options Tests
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_custom_headers(self, test_settings: Settings):
|
|
"""Test adding custom headers."""
|
|
config = {
|
|
"url": "http://test-service/api",
|
|
"method": "GET",
|
|
"headers": {
|
|
"X-Custom-Header": "custom-value",
|
|
"User-Agent": "Test-Agent"
|
|
}
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {}
|
|
mock_response.text = "Success"
|
|
|
|
mock_get = AsyncMock(return_value=mock_response)
|
|
mock_client.return_value.__aenter__.return_value.get = mock_get
|
|
|
|
result = await rest_api_executor.execute(config, test_settings)
|
|
|
|
# Verify custom headers were set
|
|
call_args = mock_get.call_args
|
|
headers = call_args.kwargs.get('headers', {})
|
|
assert headers['X-Custom-Header'] == 'custom-value'
|
|
assert headers['User-Agent'] == 'Test-Agent'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ssl_verification_disabled(self, test_settings: Settings):
|
|
"""Test disabling SSL verification."""
|
|
config = {
|
|
"url": "https://test-service/api",
|
|
"method": "GET",
|
|
"verify_ssl": False
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {}
|
|
mock_response.text = "Success"
|
|
|
|
mock_client.return_value.__aenter__.return_value.get = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
result = await rest_api_executor.execute(config, test_settings)
|
|
|
|
# Verify SSL verification was disabled
|
|
client_call = mock_client.call_args
|
|
assert client_call.kwargs.get('verify') is False
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v", "-s"])
|