http-api-tool 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- http_api_tool/__init__.py +17 -0
- http_api_tool/__main__.py +16 -0
- http_api_tool/cli.py +337 -0
- http_api_tool/verifier.py +682 -0
- http_api_tool-0.1.1.dist-info/METADATA +504 -0
- http_api_tool-0.1.1.dist-info/RECORD +9 -0
- http_api_tool-0.1.1.dist-info/WHEEL +4 -0
- http_api_tool-0.1.1.dist-info/entry_points.txt +5 -0
- http_api_tool-0.1.1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# SPDX-FileCopyrightText: 2025 The Linux Foundation
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
HTTP API Test Tool - A Python tool for HTTP/HTTPS API testing.
|
|
6
|
+
|
|
7
|
+
This package can be used both as a CLI tool (using Typer) and as a GitHub Action.
|
|
8
|
+
It uses pycurl for HTTP requests to avoid the shell escaping issues of the
|
|
9
|
+
original implementation.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.1"
|
|
13
|
+
|
|
14
|
+
from .cli import app, main
|
|
15
|
+
from .verifier import HTTPAPITester
|
|
16
|
+
|
|
17
|
+
__all__ = ["HTTPAPITester", "app", "main", "__version__"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
# SPDX-FileCopyrightText: 2025 The Linux Foundation
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Entry point script for http-api-tool.
|
|
8
|
+
|
|
9
|
+
This script provides the main entry point that can be used both as a CLI tool
|
|
10
|
+
and as a GitHub Action.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from http_api_tool import main
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
main()
|
http_api_tool/cli.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# SPDX-FileCopyrightText: 2025 The Linux Foundation
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
CLI interface for HTTP API testing.
|
|
6
|
+
|
|
7
|
+
This module provides the command-line interface using Typer for both standalone
|
|
8
|
+
CLI usage and GitHub Actions integration.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import subprocess
|
|
14
|
+
from typing import Any, Dict, Optional
|
|
15
|
+
from urllib.parse import urlparse, urlunparse
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
|
|
19
|
+
from .verifier import HTTPAPITester
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(help="A Python tool to test HTTP API services.")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_docker_host_gateway() -> Optional[str]:
|
|
25
|
+
"""Get the Docker host gateway IP that containers can use to reach the host."""
|
|
26
|
+
try:
|
|
27
|
+
# Try to get the gateway IP from the container's route table
|
|
28
|
+
result = subprocess.run(
|
|
29
|
+
["sh", "-c", "ip route | grep '^default' | cut -d' ' -f3"],
|
|
30
|
+
capture_output=True,
|
|
31
|
+
text=True,
|
|
32
|
+
timeout=5,
|
|
33
|
+
)
|
|
34
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
35
|
+
gateway_ip = result.stdout.strip()
|
|
36
|
+
if gateway_ip and gateway_ip != "localhost" and gateway_ip != "127.0.0.1":
|
|
37
|
+
return gateway_ip
|
|
38
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
39
|
+
pass
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _transform_localhost_url(url: str) -> str:
|
|
44
|
+
"""Transform localhost URLs to use Docker host gateway when running in a container."""
|
|
45
|
+
# Only transform if we're in a containerized environment (GitHub Actions)
|
|
46
|
+
if not os.environ.get("GITHUB_ACTIONS"):
|
|
47
|
+
return url
|
|
48
|
+
|
|
49
|
+
parsed = urlparse(url)
|
|
50
|
+
if parsed.hostname in ["localhost", "127.0.0.1"]:
|
|
51
|
+
gateway_ip = _get_docker_host_gateway()
|
|
52
|
+
if gateway_ip:
|
|
53
|
+
# Replace the hostname with the gateway IP
|
|
54
|
+
new_netloc = parsed.netloc.replace(parsed.hostname, gateway_ip)
|
|
55
|
+
new_parsed = parsed._replace(netloc=new_netloc)
|
|
56
|
+
transformed_url = urlunparse(new_parsed)
|
|
57
|
+
return transformed_url
|
|
58
|
+
|
|
59
|
+
return url
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.callback() # type: ignore[misc]
|
|
63
|
+
def main_callback() -> None:
|
|
64
|
+
"""
|
|
65
|
+
HTTP server/API testing tool
|
|
66
|
+
|
|
67
|
+
This script can be used both as a CLI tool (using Typer) and as a GitHub Action.
|
|
68
|
+
It uses pycurl for HTTP requests to avoid the shell escaping issues of the
|
|
69
|
+
original implementation.
|
|
70
|
+
"""
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.command("test") # type: ignore[misc]
|
|
75
|
+
def verify(
|
|
76
|
+
url: str = typer.Option(..., help="URL of API server/interface to check"),
|
|
77
|
+
auth_string: Optional[str] = typer.Option(
|
|
78
|
+
None, help="Authentication string, colon separated username/password"
|
|
79
|
+
),
|
|
80
|
+
service_name: str = typer.Option(
|
|
81
|
+
"API Service", help="Name of HTTP/HTTPS API service tested"
|
|
82
|
+
),
|
|
83
|
+
initial_sleep_time: int = typer.Option(
|
|
84
|
+
1, help="Time in seconds between API service connection attempts"
|
|
85
|
+
),
|
|
86
|
+
max_delay: int = typer.Option(30, help="Maximum delay in seconds between retries"),
|
|
87
|
+
retries: int = typer.Option(
|
|
88
|
+
3, help="Number of retries before declaring service unavailable"
|
|
89
|
+
),
|
|
90
|
+
expected_http_code: int = typer.Option(
|
|
91
|
+
200, help="HTTP response code to accept from the API service"
|
|
92
|
+
),
|
|
93
|
+
regex: Optional[str] = typer.Option(
|
|
94
|
+
None, help="Verify server response with regular expression"
|
|
95
|
+
),
|
|
96
|
+
show_header_json: bool = typer.Option(
|
|
97
|
+
False, help="Display response header as JSON in action output"
|
|
98
|
+
),
|
|
99
|
+
curl_timeout: int = typer.Option(
|
|
100
|
+
5, help="Maximum time in seconds for cURL to wait for a response"
|
|
101
|
+
),
|
|
102
|
+
http_method: str = typer.Option(
|
|
103
|
+
"GET", help="HTTP method to use (GET, POST, PUT, etc.)"
|
|
104
|
+
),
|
|
105
|
+
request_body: Optional[str] = typer.Option(
|
|
106
|
+
None, help="Data to send with POST/PUT/PATCH requests"
|
|
107
|
+
),
|
|
108
|
+
content_type: str = typer.Option(
|
|
109
|
+
"application/json", help="Content type of the request body"
|
|
110
|
+
),
|
|
111
|
+
request_headers: Optional[str] = typer.Option(
|
|
112
|
+
None, help="Custom HTTP headers sent in JSON format"
|
|
113
|
+
),
|
|
114
|
+
verify_ssl: bool = typer.Option(True, help="Verify SSL certificates"),
|
|
115
|
+
ca_bundle_path: Optional[str] = typer.Option(
|
|
116
|
+
None, help="Path to CA bundle file for SSL verification"
|
|
117
|
+
),
|
|
118
|
+
include_response_body: bool = typer.Option(
|
|
119
|
+
False, help="Include response body in outputs (base64 encoded)"
|
|
120
|
+
),
|
|
121
|
+
follow_redirects: bool = typer.Option(True, help="Follow HTTP redirects"),
|
|
122
|
+
max_response_time: float = typer.Option(
|
|
123
|
+
0, help="Maximum acceptable response time in seconds"
|
|
124
|
+
),
|
|
125
|
+
connection_reuse: bool = typer.Option(
|
|
126
|
+
True, help="Reuse connections between requests"
|
|
127
|
+
),
|
|
128
|
+
debug: bool = typer.Option(False, help="Enables debugging output"),
|
|
129
|
+
fail_on_timeout: bool = typer.Option(
|
|
130
|
+
False, help="Fail the action if response time exceeds max_response_time"
|
|
131
|
+
),
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Test HTTP API endpoint testing with retry logic."""
|
|
134
|
+
verifier = HTTPAPITester()
|
|
135
|
+
|
|
136
|
+
# Prepare config
|
|
137
|
+
config = {
|
|
138
|
+
"url": url,
|
|
139
|
+
"auth_string": auth_string,
|
|
140
|
+
"service_name": service_name,
|
|
141
|
+
"initial_sleep_time": initial_sleep_time,
|
|
142
|
+
"max_delay": max_delay,
|
|
143
|
+
"retries": retries,
|
|
144
|
+
"expected_http_code": expected_http_code,
|
|
145
|
+
"regex": regex,
|
|
146
|
+
"show_header_json": show_header_json,
|
|
147
|
+
"curl_timeout": curl_timeout,
|
|
148
|
+
"http_method": http_method,
|
|
149
|
+
"request_body": request_body,
|
|
150
|
+
"content_type": content_type,
|
|
151
|
+
"request_headers": request_headers,
|
|
152
|
+
"verify_ssl": verify_ssl,
|
|
153
|
+
"ca_bundle_path": ca_bundle_path,
|
|
154
|
+
"include_response_body": include_response_body,
|
|
155
|
+
"follow_redirects": follow_redirects,
|
|
156
|
+
"max_response_time": max_response_time,
|
|
157
|
+
"connection_reuse": connection_reuse,
|
|
158
|
+
"debug": debug,
|
|
159
|
+
"fail_on_timeout": fail_on_timeout,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Transform URL if necessary
|
|
163
|
+
if isinstance(config["url"], str):
|
|
164
|
+
config["url"] = _transform_localhost_url(config["url"])
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
result = verifier.test_api(**config)
|
|
168
|
+
|
|
169
|
+
# For CLI usage, print the results
|
|
170
|
+
if not os.environ.get("GITHUB_ACTIONS"):
|
|
171
|
+
typer.echo("✅ API test successful!")
|
|
172
|
+
typer.echo(f"Response Code: {result['response_http_code']}")
|
|
173
|
+
typer.echo(f"Total Time: {result['total_time']:.3f}s")
|
|
174
|
+
typer.echo(f"Connect Time: {result['connect_time']:.3f}s")
|
|
175
|
+
if result.get("regex_match"):
|
|
176
|
+
typer.echo(f"Regex Match: {'✅' if result['regex_match'] else '❌'}")
|
|
177
|
+
|
|
178
|
+
except Exception as e:
|
|
179
|
+
typer.echo(f"Error: {e}", err=True)
|
|
180
|
+
raise typer.Exit(1)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _log_action_parameters(config: Dict[str, Any]) -> None:
|
|
184
|
+
"""Log the action parameters in a user-friendly format."""
|
|
185
|
+
from .verifier import HTTPAPITester
|
|
186
|
+
|
|
187
|
+
# Create a temporary verifier instance to use sanitization methods
|
|
188
|
+
temp_verifier = HTTPAPITester()
|
|
189
|
+
|
|
190
|
+
print("📋 Configuration:")
|
|
191
|
+
# Sanitize URL for logging
|
|
192
|
+
url = config.get("url", "Not specified")
|
|
193
|
+
if url != "Not specified":
|
|
194
|
+
url = temp_verifier.sanitize_url_for_logging(url)
|
|
195
|
+
print(f" URL: {url}")
|
|
196
|
+
print(f" HTTP Method: {config.get('http_method', 'GET')}")
|
|
197
|
+
print(f" Service Name: {config.get('service_name', 'API Service')}")
|
|
198
|
+
print(f" Expected HTTP Code: {config.get('expected_http_code', '200')}")
|
|
199
|
+
print(f" Retries: {config.get('retries', '3')}")
|
|
200
|
+
print(f" Timeout: {config.get('curl_timeout', '5')} seconds")
|
|
201
|
+
print(f" SSL Verification: {config.get('verify_ssl', 'true')}")
|
|
202
|
+
print(f" Follow Redirects: {config.get('follow_redirects', 'true')}")
|
|
203
|
+
|
|
204
|
+
# Show optional parameters if they're set
|
|
205
|
+
if config.get("regex"):
|
|
206
|
+
print(f" Regex Pattern: {config['regex']}")
|
|
207
|
+
if config.get("request_body"):
|
|
208
|
+
body = config["request_body"]
|
|
209
|
+
sanitized_body = temp_verifier.sanitize_request_body_for_logging(body, 100)
|
|
210
|
+
print(f" Request Body: {sanitized_body}")
|
|
211
|
+
if config.get("request_headers"):
|
|
212
|
+
sanitized_headers = temp_verifier.sanitize_headers_for_logging(
|
|
213
|
+
config["request_headers"]
|
|
214
|
+
)
|
|
215
|
+
print(f" Custom Headers: {sanitized_headers}")
|
|
216
|
+
if config.get("auth_string"):
|
|
217
|
+
print(" Authentication: *** (hidden)")
|
|
218
|
+
max_time = config.get("max_response_time")
|
|
219
|
+
if max_time and float(max_time) > 0:
|
|
220
|
+
print(f" Max Response Time: {max_time} seconds")
|
|
221
|
+
|
|
222
|
+
debug_enabled = config.get("debug", "false").lower() == "true"
|
|
223
|
+
print(f" Debug Mode: {debug_enabled}")
|
|
224
|
+
print("=" * 50)
|
|
225
|
+
print()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def run_github_action() -> None:
|
|
229
|
+
"""Run in GitHub Actions mode."""
|
|
230
|
+
verifier = HTTPAPITester()
|
|
231
|
+
|
|
232
|
+
# Print startup banner
|
|
233
|
+
print("🚀 HTTP API Tool")
|
|
234
|
+
print("=" * 50)
|
|
235
|
+
|
|
236
|
+
# Map GitHub Action inputs to function parameters
|
|
237
|
+
config = {}
|
|
238
|
+
input_mappings = {
|
|
239
|
+
"url": "INPUT_URL",
|
|
240
|
+
"auth_string": "INPUT_AUTH_STRING",
|
|
241
|
+
"service_name": "INPUT_SERVICE_NAME",
|
|
242
|
+
"initial_sleep_time": "INPUT_INITIAL_SLEEP_TIME",
|
|
243
|
+
"max_delay": "INPUT_MAX_DELAY",
|
|
244
|
+
"retries": "INPUT_RETRIES",
|
|
245
|
+
"expected_http_code": "INPUT_EXPECTED_HTTP_CODE",
|
|
246
|
+
"regex": "INPUT_REGEX",
|
|
247
|
+
"show_header_json": "INPUT_SHOW_HEADER_JSON",
|
|
248
|
+
"curl_timeout": "INPUT_CURL_TIMEOUT",
|
|
249
|
+
"http_method": "INPUT_HTTP_METHOD",
|
|
250
|
+
"request_body": "INPUT_REQUEST_BODY",
|
|
251
|
+
"content_type": "INPUT_CONTENT_TYPE",
|
|
252
|
+
"request_headers": "INPUT_REQUEST_HEADERS",
|
|
253
|
+
"verify_ssl": "INPUT_VERIFY_SSL",
|
|
254
|
+
"ca_bundle_path": "INPUT_CA_BUNDLE_PATH",
|
|
255
|
+
"include_response_body": "INPUT_INCLUDE_RESPONSE_BODY",
|
|
256
|
+
"follow_redirects": "INPUT_FOLLOW_REDIRECTS",
|
|
257
|
+
"max_response_time": "INPUT_MAX_RESPONSE_TIME",
|
|
258
|
+
"connection_reuse": "INPUT_CONNECTION_REUSE",
|
|
259
|
+
"debug": "INPUT_DEBUG",
|
|
260
|
+
"fail_on_timeout": "INPUT_FAIL_ON_TIMEOUT",
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
# Set defaults
|
|
264
|
+
defaults = {
|
|
265
|
+
"service_name": "API Service",
|
|
266
|
+
"initial_sleep_time": "1",
|
|
267
|
+
"max_delay": "30",
|
|
268
|
+
"retries": "3",
|
|
269
|
+
"expected_http_code": "200",
|
|
270
|
+
"curl_timeout": "5",
|
|
271
|
+
"http_method": "GET",
|
|
272
|
+
"content_type": "application/json",
|
|
273
|
+
"verify_ssl": "true",
|
|
274
|
+
"include_response_body": "false",
|
|
275
|
+
"follow_redirects": "true",
|
|
276
|
+
"max_response_time": "0",
|
|
277
|
+
"connection_reuse": "true",
|
|
278
|
+
"debug": "false",
|
|
279
|
+
"fail_on_timeout": "false",
|
|
280
|
+
"show_header_json": "false",
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
# Get inputs from environment with defaults
|
|
284
|
+
for param, env_var in input_mappings.items():
|
|
285
|
+
value = os.environ.get(env_var, defaults.get(param))
|
|
286
|
+
if value is not None:
|
|
287
|
+
config[param] = value
|
|
288
|
+
|
|
289
|
+
# Transform localhost URLs for Docker container networking
|
|
290
|
+
if "url" in config:
|
|
291
|
+
config["url"] = _transform_localhost_url(config["url"])
|
|
292
|
+
|
|
293
|
+
# Log the configuration
|
|
294
|
+
_log_action_parameters(config)
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
result = verifier.test_api(**config)
|
|
298
|
+
|
|
299
|
+
# Write outputs to GitHub Actions
|
|
300
|
+
for key, value in result.items():
|
|
301
|
+
# Convert boolean values to lowercase strings for GitHub Actions
|
|
302
|
+
# Note: result dict contains mixed types: bool, int, float, str
|
|
303
|
+
value_any: Any = value # Help MyPy understand mixed types
|
|
304
|
+
if isinstance(value_any, bool):
|
|
305
|
+
output_value = str(value_any).lower()
|
|
306
|
+
else:
|
|
307
|
+
# Handle non-boolean values (int, float, str, etc.)
|
|
308
|
+
output_value = str(value_any)
|
|
309
|
+
verifier.write_github_output(key, output_value)
|
|
310
|
+
|
|
311
|
+
# Show completion message
|
|
312
|
+
print()
|
|
313
|
+
print("✅ HTTP API Tool")
|
|
314
|
+
print("=" * 50)
|
|
315
|
+
|
|
316
|
+
except Exception as e:
|
|
317
|
+
# Only log once to avoid duplication
|
|
318
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
319
|
+
sys.exit(1)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def main() -> None:
|
|
323
|
+
"""Main entry point - handles both GitHub Actions and CLI usage."""
|
|
324
|
+
# Check if we're in GitHub Actions context AND not being invoked for help
|
|
325
|
+
# AND no CLI subcommands are provided (indicating genuine GitHub Actions usage)
|
|
326
|
+
has_cli_command = any(cmd in sys.argv for cmd in ["test", "help", "--help", "-h"])
|
|
327
|
+
|
|
328
|
+
if (
|
|
329
|
+
os.environ.get("GITHUB_ACTIONS")
|
|
330
|
+
and not has_cli_command
|
|
331
|
+
and "--help" not in sys.argv
|
|
332
|
+
and "-h" not in sys.argv
|
|
333
|
+
):
|
|
334
|
+
run_github_action()
|
|
335
|
+
else:
|
|
336
|
+
# Running as CLI tool or help was requested or explicit CLI command provided
|
|
337
|
+
app()
|