mirror of
https://github.com/httpie/cli.git
synced 2025-03-13 14:28:50 +01:00
Merge 5cc4edc9bd
into 5b604c37c6
This commit is contained in:
commit
5ddded8990
@ -203,6 +203,10 @@ class HTTPieArgumentParser(BaseHTTPieArgumentParser):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _process_url(self):
|
def _process_url(self):
|
||||||
|
if self.args.http_file:
|
||||||
|
# do not add default scheme
|
||||||
|
# treat URL as a filename if --http-file is specified
|
||||||
|
return
|
||||||
if self.args.url.startswith('://'):
|
if self.args.url.startswith('://'):
|
||||||
# Paste URL & add space shortcut: `http ://pie.dev` → `http://pie.dev`
|
# Paste URL & add space shortcut: `http ://pie.dev` → `http://pie.dev`
|
||||||
self.args.url = self.args.url[3:]
|
self.args.url = self.args.url[3:]
|
||||||
|
File diff suppressed because it is too large
Load Diff
329
httpie/core.py
329
httpie/core.py
@ -3,7 +3,7 @@ import os
|
|||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
import socket
|
import socket
|
||||||
from typing import List, Optional, Union, Callable
|
from typing import List, Optional, Union, Callable, Iterable, Dict, Tuple
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from pygments import __version__ as pygments_version
|
from pygments import __version__ as pygments_version
|
||||||
@ -12,20 +12,29 @@ from requests import __version__ as requests_version
|
|||||||
from . import __version__ as httpie_version
|
from . import __version__ as httpie_version
|
||||||
from .cli.constants import OUT_REQ_BODY
|
from .cli.constants import OUT_REQ_BODY
|
||||||
from .cli.nested_json import NestedJSONSyntaxError
|
from .cli.nested_json import NestedJSONSyntaxError
|
||||||
from .client import collect_messages
|
from .client import collect_messages, RequestsMessage
|
||||||
from .context import Environment, LogLevel
|
from .context import Environment, LogLevel
|
||||||
from .downloads import Downloader
|
from .downloads import Downloader
|
||||||
from .models import (
|
from .http_parser import (
|
||||||
RequestsMessageKind,
|
parse_single_request,
|
||||||
OutputOptions
|
replace_global,
|
||||||
|
split_requests,
|
||||||
|
replace_dependencies
|
||||||
)
|
)
|
||||||
|
from .models import RequestsMessageKind, OutputOptions
|
||||||
from .output.models import ProcessingOptions
|
from .output.models import ProcessingOptions
|
||||||
from .output.writer import write_message, write_stream, write_raw_data, MESSAGE_SEPARATOR_BYTES
|
from .output.writer import (
|
||||||
|
write_message,
|
||||||
|
write_stream,
|
||||||
|
write_raw_data,
|
||||||
|
MESSAGE_SEPARATOR_BYTES,
|
||||||
|
)
|
||||||
from .plugins.registry import plugin_manager
|
from .plugins.registry import plugin_manager
|
||||||
from .status import ExitStatus, http_status_to_exit_status
|
from .status import ExitStatus, http_status_to_exit_status
|
||||||
from .utils import unwrap_context
|
from .utils import unwrap_context
|
||||||
from .internal.update_warnings import check_updates
|
from .internal.update_warnings import check_updates
|
||||||
from .internal.daemon_runner import is_daemon_mode, run_daemon_task
|
from .internal.daemon_runner import is_daemon_mode, run_daemon_task
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyDefaultArgument
|
# noinspection PyDefaultArgument
|
||||||
@ -48,27 +57,27 @@ def raw_main(
|
|||||||
if use_default_options and env.config.default_options:
|
if use_default_options and env.config.default_options:
|
||||||
args = env.config.default_options + args
|
args = env.config.default_options + args
|
||||||
|
|
||||||
include_debug_info = '--debug' in args
|
include_debug_info = "--debug" in args
|
||||||
include_traceback = include_debug_info or '--traceback' in args
|
include_traceback = include_debug_info or "--traceback" in args
|
||||||
|
|
||||||
def handle_generic_error(e, annotation=None):
|
def handle_generic_error(e, annotation=None):
|
||||||
msg = str(e)
|
msg = str(e)
|
||||||
if hasattr(e, 'request'):
|
if hasattr(e, "request"):
|
||||||
request = e.request
|
request = e.request
|
||||||
if hasattr(request, 'url'):
|
if hasattr(request, "url"):
|
||||||
msg = (
|
msg = (
|
||||||
f'{msg} while doing a {request.method}'
|
f"{msg} while doing a {request.method}"
|
||||||
f' request to URL: {request.url}'
|
f" request to URL: {request.url}"
|
||||||
)
|
)
|
||||||
if annotation:
|
if annotation:
|
||||||
msg += annotation
|
msg += annotation
|
||||||
env.log_error(f'{type(e).__name__}: {msg}')
|
env.log_error(f"{type(e).__name__}: {msg}")
|
||||||
if include_traceback:
|
if include_traceback:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if include_debug_info:
|
if include_debug_info:
|
||||||
print_debug_info(env)
|
print_debug_info(env)
|
||||||
if args == ['--debug']:
|
if args == ["--debug"]:
|
||||||
return ExitStatus.SUCCESS
|
return ExitStatus.SUCCESS
|
||||||
|
|
||||||
exit_status = ExitStatus.SUCCESS
|
exit_status = ExitStatus.SUCCESS
|
||||||
@ -84,13 +93,13 @@ def raw_main(
|
|||||||
raise
|
raise
|
||||||
exit_status = ExitStatus.ERROR
|
exit_status = ExitStatus.ERROR
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
env.stderr.write('\n')
|
env.stderr.write("\n")
|
||||||
if include_traceback:
|
if include_traceback:
|
||||||
raise
|
raise
|
||||||
exit_status = ExitStatus.ERROR_CTRL_C
|
exit_status = ExitStatus.ERROR_CTRL_C
|
||||||
except SystemExit as e:
|
except SystemExit as e:
|
||||||
if e.code != ExitStatus.SUCCESS:
|
if e.code != ExitStatus.SUCCESS:
|
||||||
env.stderr.write('\n')
|
env.stderr.write("\n")
|
||||||
if include_traceback:
|
if include_traceback:
|
||||||
raise
|
raise
|
||||||
exit_status = ExitStatus.ERROR
|
exit_status = ExitStatus.ERROR
|
||||||
@ -102,33 +111,32 @@ def raw_main(
|
|||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
env.stderr.write('\n')
|
env.stderr.write("\n")
|
||||||
if include_traceback:
|
if include_traceback:
|
||||||
raise
|
raise
|
||||||
exit_status = ExitStatus.ERROR_CTRL_C
|
exit_status = ExitStatus.ERROR_CTRL_C
|
||||||
except SystemExit as e:
|
except SystemExit as e:
|
||||||
if e.code != ExitStatus.SUCCESS:
|
if e.code != ExitStatus.SUCCESS:
|
||||||
env.stderr.write('\n')
|
env.stderr.write("\n")
|
||||||
if include_traceback:
|
if include_traceback:
|
||||||
raise
|
raise
|
||||||
exit_status = ExitStatus.ERROR
|
exit_status = ExitStatus.ERROR
|
||||||
except requests.Timeout:
|
except requests.Timeout:
|
||||||
exit_status = ExitStatus.ERROR_TIMEOUT
|
exit_status = ExitStatus.ERROR_TIMEOUT
|
||||||
env.log_error(f'Request timed out ({parsed_args.timeout}s).')
|
env.log_error(f"Request timed out ({parsed_args.timeout}s).")
|
||||||
except requests.TooManyRedirects:
|
except requests.TooManyRedirects:
|
||||||
exit_status = ExitStatus.ERROR_TOO_MANY_REDIRECTS
|
exit_status = ExitStatus.ERROR_TOO_MANY_REDIRECTS
|
||||||
env.log_error(
|
env.log_error(
|
||||||
f'Too many redirects'
|
f"Too many redirects (--max-redirects={parsed_args.max_redirects})."
|
||||||
f' (--max-redirects={parsed_args.max_redirects}).'
|
|
||||||
)
|
)
|
||||||
except requests.exceptions.ConnectionError as exc:
|
except requests.exceptions.ConnectionError as exc:
|
||||||
annotation = None
|
annotation = None
|
||||||
original_exc = unwrap_context(exc)
|
original_exc = unwrap_context(exc)
|
||||||
if isinstance(original_exc, socket.gaierror):
|
if isinstance(original_exc, socket.gaierror):
|
||||||
if original_exc.errno == socket.EAI_AGAIN:
|
if original_exc.errno == socket.EAI_AGAIN:
|
||||||
annotation = '\nCouldn’t connect to a DNS server. Please check your connection and try again.'
|
annotation = "\nCouldn’t connect to a DNS server. Please check your connection and try again."
|
||||||
elif original_exc.errno == socket.EAI_NONAME:
|
elif original_exc.errno == socket.EAI_NONAME:
|
||||||
annotation = '\nCouldn’t resolve the given hostname. Please check the URL and try again.'
|
annotation = "\nCouldn’t resolve the given hostname. Please check the URL and try again."
|
||||||
propagated_exc = original_exc
|
propagated_exc = original_exc
|
||||||
else:
|
else:
|
||||||
propagated_exc = exc
|
propagated_exc = exc
|
||||||
@ -144,8 +152,7 @@ def raw_main(
|
|||||||
|
|
||||||
|
|
||||||
def main(
|
def main(
|
||||||
args: List[Union[str, bytes]] = sys.argv,
|
args: List[Union[str, bytes]] = sys.argv, env: Environment = Environment()
|
||||||
env: Environment = Environment()
|
|
||||||
) -> ExitStatus:
|
) -> ExitStatus:
|
||||||
"""
|
"""
|
||||||
The main function.
|
The main function.
|
||||||
@ -159,12 +166,7 @@ def main(
|
|||||||
|
|
||||||
from .cli.definition import parser
|
from .cli.definition import parser
|
||||||
|
|
||||||
return raw_main(
|
return raw_main(parser=parser, main_program=program, args=args, env=env)
|
||||||
parser=parser,
|
|
||||||
main_program=program,
|
|
||||||
args=args,
|
|
||||||
env=env
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
||||||
@ -172,127 +174,178 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
|||||||
The main program without error handling.
|
The main program without error handling.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# TODO: Refactor and drastically simplify, especially so that the separator logic is elsewhere.
|
|
||||||
exit_status = ExitStatus.SUCCESS
|
|
||||||
downloader = None
|
|
||||||
initial_request: Optional[requests.PreparedRequest] = None
|
|
||||||
final_response: Optional[requests.Response] = None
|
|
||||||
processing_options = ProcessingOptions.from_raw_args(args)
|
|
||||||
|
|
||||||
def separate():
|
def actual_program(args: argparse.Namespace, env: Environment) -> Tuple[ExitStatus, Iterable[RequestsMessage]]:
|
||||||
getattr(env.stdout, 'buffer', env.stdout).write(MESSAGE_SEPARATOR_BYTES)
|
# TODO: Refactor and drastically simplify, especially so that the separator logic is elsewhere.
|
||||||
|
exit_status = ExitStatus.SUCCESS
|
||||||
|
downloader = None
|
||||||
|
initial_request: Optional[requests.PreparedRequest] = None
|
||||||
|
final_response: Optional[requests.Response] = None
|
||||||
|
processing_options = ProcessingOptions.from_raw_args(args)
|
||||||
|
|
||||||
def request_body_read_callback(chunk: bytes):
|
def separate():
|
||||||
should_pipe_to_stdout = bool(
|
getattr(env.stdout, "buffer", env.stdout).write(MESSAGE_SEPARATOR_BYTES)
|
||||||
# Request body output desired
|
|
||||||
OUT_REQ_BODY in args.output_options
|
def request_body_read_callback(chunk: bytes):
|
||||||
# & not `.read()` already pre-request (e.g., for compression)
|
should_pipe_to_stdout = bool(
|
||||||
and initial_request
|
# Request body output desired
|
||||||
# & non-EOF chunk
|
OUT_REQ_BODY in args.output_options
|
||||||
and chunk
|
# & not `.read()` already pre-request (e.g., for compression)
|
||||||
)
|
and initial_request
|
||||||
if should_pipe_to_stdout:
|
# & non-EOF chunk
|
||||||
return write_raw_data(
|
and chunk
|
||||||
env,
|
|
||||||
chunk,
|
|
||||||
processing_options=processing_options,
|
|
||||||
headers=initial_request.headers
|
|
||||||
)
|
)
|
||||||
|
if should_pipe_to_stdout:
|
||||||
try:
|
return write_raw_data(
|
||||||
if args.download:
|
env,
|
||||||
args.follow = True # --download implies --follow.
|
chunk,
|
||||||
downloader = Downloader(env, output_file=args.output_file, resume=args.download_resume)
|
processing_options=processing_options,
|
||||||
downloader.pre_request(args.headers)
|
headers=initial_request.headers,
|
||||||
messages = collect_messages(env, args=args,
|
|
||||||
request_body_read_callback=request_body_read_callback)
|
|
||||||
force_separator = False
|
|
||||||
prev_with_body = False
|
|
||||||
|
|
||||||
# Process messages as they’re generated
|
|
||||||
for message in messages:
|
|
||||||
output_options = OutputOptions.from_message(message, args.output_options)
|
|
||||||
|
|
||||||
do_write_body = output_options.body
|
|
||||||
if prev_with_body and output_options.any() and (force_separator or not env.stdout_isatty):
|
|
||||||
# Separate after a previous message with body, if needed. See test_tokens.py.
|
|
||||||
separate()
|
|
||||||
force_separator = False
|
|
||||||
if output_options.kind is RequestsMessageKind.REQUEST:
|
|
||||||
if not initial_request:
|
|
||||||
initial_request = message
|
|
||||||
if output_options.body:
|
|
||||||
is_streamed_upload = not isinstance(message.body, (str, bytes))
|
|
||||||
do_write_body = not is_streamed_upload
|
|
||||||
force_separator = is_streamed_upload and env.stdout_isatty
|
|
||||||
else:
|
|
||||||
final_response = message
|
|
||||||
if args.check_status or downloader:
|
|
||||||
exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow)
|
|
||||||
if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet == 1):
|
|
||||||
env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level=LogLevel.WARNING)
|
|
||||||
write_message(
|
|
||||||
requests_message=message,
|
|
||||||
env=env,
|
|
||||||
output_options=output_options._replace(
|
|
||||||
body=do_write_body
|
|
||||||
),
|
|
||||||
processing_options=processing_options
|
|
||||||
)
|
|
||||||
prev_with_body = output_options.body
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
if force_separator:
|
|
||||||
separate()
|
|
||||||
if downloader and exit_status == ExitStatus.SUCCESS:
|
|
||||||
# Last response body download.
|
|
||||||
download_stream, download_to = downloader.start(
|
|
||||||
initial_url=initial_request.url,
|
|
||||||
final_response=final_response,
|
|
||||||
)
|
|
||||||
write_stream(stream=download_stream, outfile=download_to, flush=False)
|
|
||||||
downloader.finish()
|
|
||||||
if downloader.interrupted:
|
|
||||||
exit_status = ExitStatus.ERROR
|
|
||||||
env.log_error(
|
|
||||||
f'Incomplete download: size={downloader.status.total_size};'
|
|
||||||
f' downloaded={downloader.status.downloaded}'
|
|
||||||
)
|
)
|
||||||
return exit_status
|
|
||||||
|
|
||||||
finally:
|
try:
|
||||||
if downloader and not downloader.finished:
|
if args.download:
|
||||||
downloader.failed()
|
args.follow = True # --download implies --follow.
|
||||||
if args.output_file and args.output_file_specified:
|
downloader = Downloader(
|
||||||
args.output_file.close()
|
env, output_file=args.output_file, resume=args.download_resume
|
||||||
|
)
|
||||||
|
downloader.pre_request(args.headers)
|
||||||
|
|
||||||
|
messages = collect_messages(
|
||||||
|
env, args=args, request_body_read_callback=request_body_read_callback
|
||||||
|
)
|
||||||
|
force_separator = False
|
||||||
|
prev_with_body = False
|
||||||
|
|
||||||
|
# Process messages as they’re generated
|
||||||
|
for message in messages:
|
||||||
|
output_options = OutputOptions.from_message(
|
||||||
|
message, args.output_options
|
||||||
|
)
|
||||||
|
|
||||||
|
do_write_body = output_options.body
|
||||||
|
if (
|
||||||
|
prev_with_body
|
||||||
|
and output_options.any()
|
||||||
|
and (force_separator or not env.stdout_isatty)
|
||||||
|
):
|
||||||
|
# Separate after a previous message with body, if needed. See test_tokens.py.
|
||||||
|
separate()
|
||||||
|
force_separator = False
|
||||||
|
if output_options.kind is RequestsMessageKind.REQUEST:
|
||||||
|
if not initial_request:
|
||||||
|
initial_request = message
|
||||||
|
if output_options.body:
|
||||||
|
is_streamed_upload = not isinstance(message.body, (str, bytes))
|
||||||
|
do_write_body = not is_streamed_upload
|
||||||
|
force_separator = is_streamed_upload and env.stdout_isatty
|
||||||
|
else:
|
||||||
|
final_response = message
|
||||||
|
if args.check_status or downloader:
|
||||||
|
exit_status = http_status_to_exit_status(
|
||||||
|
http_status=message.status_code, follow=args.follow
|
||||||
|
)
|
||||||
|
if exit_status != ExitStatus.SUCCESS and (
|
||||||
|
not env.stdout_isatty or args.quiet == 1
|
||||||
|
):
|
||||||
|
env.log_error(
|
||||||
|
f"HTTP {message.raw.status} {message.raw.reason}",
|
||||||
|
level=LogLevel.WARNING,
|
||||||
|
)
|
||||||
|
write_message(
|
||||||
|
requests_message=message,
|
||||||
|
env=env,
|
||||||
|
output_options=output_options._replace(body=do_write_body),
|
||||||
|
processing_options=processing_options,
|
||||||
|
)
|
||||||
|
prev_with_body = output_options.body
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
if force_separator:
|
||||||
|
separate()
|
||||||
|
if downloader and exit_status == ExitStatus.SUCCESS:
|
||||||
|
# Last response body download.
|
||||||
|
download_stream, download_to = downloader.start(
|
||||||
|
initial_url=initial_request.url,
|
||||||
|
final_response=final_response,
|
||||||
|
)
|
||||||
|
write_stream(stream=download_stream, outfile=download_to, flush=False)
|
||||||
|
downloader.finish()
|
||||||
|
if downloader.interrupted:
|
||||||
|
exit_status = ExitStatus.ERROR
|
||||||
|
env.log_error(
|
||||||
|
f"Incomplete download: size={downloader.status.total_size};"
|
||||||
|
f" downloaded={downloader.status.downloaded}"
|
||||||
|
)
|
||||||
|
return exit_status, messages
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if downloader and not downloader.finished:
|
||||||
|
downloader.failed()
|
||||||
|
if args.output_file and args.output_file_specified:
|
||||||
|
args.output_file.close()
|
||||||
|
|
||||||
|
if args.http_file:
|
||||||
|
http_file = Path(args.url)
|
||||||
|
if not http_file.exists():
|
||||||
|
raise FileNotFoundError(f"File not found: {args.url}")
|
||||||
|
if not http_file.is_file():
|
||||||
|
raise IsADirectoryError(f"Path is not a file: {args.url}")
|
||||||
|
http_contents = http_file.read_text()
|
||||||
|
|
||||||
|
raw_requests = split_requests(replace_global(http_contents))
|
||||||
|
raw_requests = [req.strip() for req in raw_requests if req.strip()]
|
||||||
|
parsed_requests = []
|
||||||
|
req_names = []
|
||||||
|
responses: Dict[str, RequestsMessage] = {}
|
||||||
|
Exit_status = []
|
||||||
|
|
||||||
|
for raw_req in raw_requests:
|
||||||
|
dependency_free_req = replace_dependencies(raw_req, responses)
|
||||||
|
|
||||||
|
new_req = parse_single_request(dependency_free_req)
|
||||||
|
if new_req is None:
|
||||||
|
continue
|
||||||
|
if new_req.name is not None:
|
||||||
|
req_names.append(new_req.name)
|
||||||
|
parsed_requests.append(new_req)
|
||||||
|
args.url = new_req.url
|
||||||
|
args.method = new_req.method
|
||||||
|
args.headers = new_req.headers
|
||||||
|
args.data = new_req.body
|
||||||
|
|
||||||
|
status, response = actual_program(args, env)
|
||||||
|
Exit_status.append(status)
|
||||||
|
if new_req.name is not None:
|
||||||
|
responses[new_req.name] = response
|
||||||
|
|
||||||
|
all_success = all(r is ExitStatus.SUCCESS for r in Exit_status)
|
||||||
|
return ExitStatus.SUCCESS if all_success else ExitStatus.ERROR
|
||||||
|
|
||||||
|
return actual_program(args, env)[0]
|
||||||
|
|
||||||
|
|
||||||
def print_debug_info(env: Environment):
|
def print_debug_info(env: Environment):
|
||||||
env.stderr.writelines([
|
env.stderr.writelines(
|
||||||
f'HTTPie {httpie_version}\n',
|
[
|
||||||
f'Requests {requests_version}\n',
|
f"HTTPie {httpie_version}\n",
|
||||||
f'Pygments {pygments_version}\n',
|
f"Requests {requests_version}\n",
|
||||||
f'Python {sys.version}\n{sys.executable}\n',
|
f"Pygments {pygments_version}\n",
|
||||||
f'{platform.system()} {platform.release()}',
|
f"Python {sys.version}\n{sys.executable}\n",
|
||||||
])
|
f"{platform.system()} {platform.release()}",
|
||||||
env.stderr.write('\n\n')
|
]
|
||||||
|
)
|
||||||
|
env.stderr.write("\n\n")
|
||||||
env.stderr.write(repr(env))
|
env.stderr.write(repr(env))
|
||||||
env.stderr.write('\n\n')
|
env.stderr.write("\n\n")
|
||||||
env.stderr.write(repr(plugin_manager))
|
env.stderr.write(repr(plugin_manager))
|
||||||
env.stderr.write('\n')
|
env.stderr.write("\n")
|
||||||
|
|
||||||
|
|
||||||
def decode_raw_args(
|
def decode_raw_args(args: List[Union[str, bytes]], stdin_encoding: str) -> List[str]:
|
||||||
args: List[Union[str, bytes]],
|
|
||||||
stdin_encoding: str
|
|
||||||
) -> List[str]:
|
|
||||||
"""
|
"""
|
||||||
Convert all bytes args to str
|
Convert all bytes args to str
|
||||||
by decoding them using stdin encoding.
|
by decoding them using stdin encoding.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return [
|
return [arg.decode(stdin_encoding) if type(arg) is bytes else arg for arg in args]
|
||||||
arg.decode(stdin_encoding)
|
|
||||||
if type(arg) is bytes else arg
|
|
||||||
for arg in args
|
|
||||||
]
|
|
||||||
|
167
httpie/http_parser.py
Normal file
167
httpie/http_parser.py
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import re
|
||||||
|
from re import Match
|
||||||
|
from .client import RequestsMessage
|
||||||
|
from typing import Iterable, Dict, List
|
||||||
|
import json
|
||||||
|
from jsonpath_ng import parse as jsonpath_parse
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HttpFileRequest:
|
||||||
|
method: str
|
||||||
|
url: str
|
||||||
|
headers: Dict | None
|
||||||
|
body: bytes | None
|
||||||
|
name: str | None
|
||||||
|
|
||||||
|
|
||||||
|
def split_requests(http_file_contents: str) -> List[str]:
|
||||||
|
"""Splits an HTTP file into individual requests but keeps the '###' in each request."""
|
||||||
|
parts = re.split(r"(^###.*)", http_file_contents, flags=re.MULTILINE)
|
||||||
|
requests = []
|
||||||
|
|
||||||
|
for i in range(1, len(parts), 2):
|
||||||
|
header = parts[i].strip()
|
||||||
|
body = parts[i + 1].strip() if i + 1 < len(parts) else ""
|
||||||
|
requests.append(f"{header}\n{body}")
|
||||||
|
|
||||||
|
return requests
|
||||||
|
|
||||||
|
|
||||||
|
def replace_dependencies(raw_http_request: str, responses: Dict[str, Iterable[RequestsMessage]]) -> str | None:
|
||||||
|
"""Replaces the dependencies dependent variables in the raw request with their values"""
|
||||||
|
def replace(match: Match[str]):
|
||||||
|
"""gives the string which should replaces the one given as a parameter"""
|
||||||
|
str = match.group(0)
|
||||||
|
var = str.lstrip("{").rstrip("}")
|
||||||
|
splitter = re.match(r"(?P<name>\w+)\.(?P<type>request|response)\.(?P<section>body|headers)\.(?P<extractor>.+)", var)
|
||||||
|
if not splitter:
|
||||||
|
raise ValueError(f"Difficulties replacing {str} in {raw_http_request}")
|
||||||
|
Dict = splitter.groupdict()
|
||||||
|
req_name = Dict["name"]
|
||||||
|
req_type = Dict["type"]
|
||||||
|
section = Dict["section"]
|
||||||
|
extractor = Dict["extractor"]
|
||||||
|
|
||||||
|
if responses.get(req_name) is None:
|
||||||
|
raise ValueError(f"{req_name} is not an existing request's name")
|
||||||
|
if req_type == "request":
|
||||||
|
msg = responses[req_name][0]
|
||||||
|
elif req_type == "response":
|
||||||
|
msg: RequestsMessage = responses[req_name][1]
|
||||||
|
if section == "body":
|
||||||
|
if extractor == "*":
|
||||||
|
return msg.body # Return full body
|
||||||
|
elif extractor.startswith("$."): # JSONPath
|
||||||
|
try:
|
||||||
|
json_data = msg.json() # Convert response to JSON
|
||||||
|
jsonpath_expr = jsonpath_parse(extractor)
|
||||||
|
parsed_data = jsonpath_expr.find(json_data)
|
||||||
|
return [matched.value for matched in parsed_data] if parsed_data else None
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return None # Not a valid JSON
|
||||||
|
elif extractor.startswith("/"): # XPath
|
||||||
|
try:
|
||||||
|
xml_tree = etree.fromstring(msg.content) # Parse XML
|
||||||
|
return xml_tree.xpath(extractor)
|
||||||
|
except etree.XMLSyntaxError:
|
||||||
|
return None # Not a valid XML
|
||||||
|
|
||||||
|
elif section == "headers":
|
||||||
|
return msg.headers[extractor]
|
||||||
|
raise ValueError(f"Incoherent request {str}")
|
||||||
|
pattern = r"\{\{(.*?)\}\}"
|
||||||
|
return re.sub(pattern, replace, raw_http_request)
|
||||||
|
|
||||||
|
|
||||||
|
def get_name(raw_http_request: str) -> str | None:
|
||||||
|
"""
|
||||||
|
Returns the name of the HTTP request if it has one, None otherwise.
|
||||||
|
The expected pattern is either a comment starting with '//' or '#' (optionally preceded by whitespace)
|
||||||
|
followed by '@name' and the name.
|
||||||
|
"""
|
||||||
|
# Allow leading whitespace before the comment marker.
|
||||||
|
matches = re.findall(r"^\s*(?://|#)\s*@name\s+(.+)$", raw_http_request, re.MULTILINE)
|
||||||
|
|
||||||
|
if len(matches) == 0:
|
||||||
|
return None
|
||||||
|
elif len(matches) == 1:
|
||||||
|
return matches[0].strip() # strip extra whitespace if any
|
||||||
|
else:
|
||||||
|
# TODO: Handle error for multiple names found. Currently returns None.
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def replace_global(http_file_contents_raw: str) -> str:
|
||||||
|
"""finds and replaces all global variables by their values"""
|
||||||
|
# possible error when @variable=value is in the body
|
||||||
|
matches = re.findall(r"^@([A-Za-z0-9_]+)=(.+)$", http_file_contents_raw, flags=re.MULTILINE)
|
||||||
|
http_file_contents_cooking = http_file_contents_raw
|
||||||
|
for variableName, value in matches:
|
||||||
|
http_file_contents_cooking = re.sub(
|
||||||
|
rf"{{{{({re.escape(variableName)})}}}}", value, http_file_contents_cooking
|
||||||
|
)
|
||||||
|
return http_file_contents_cooking
|
||||||
|
|
||||||
|
|
||||||
|
def extract_headers(raw_text: List[str]) -> Dict:
|
||||||
|
"""
|
||||||
|
Extract the headers of the .http file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
raw_text: the lines of the .http file containing the headers
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: containing the parsed headers
|
||||||
|
"""
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
for line in raw_text:
|
||||||
|
if not line.strip() or ':' not in line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
header_name, header_value = line.split(':', 1)
|
||||||
|
|
||||||
|
headers[header_name.strip()] = header_value.strip()
|
||||||
|
|
||||||
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
def parse_body(raw_text: str) -> bytes:
|
||||||
|
"""
|
||||||
|
parse the body of the .http file
|
||||||
|
"""
|
||||||
|
return b""
|
||||||
|
|
||||||
|
|
||||||
|
def parse_single_request(raw_text: str) -> HttpFileRequest:
|
||||||
|
"""Parse a single request from .http file format to HttpFileRequest """
|
||||||
|
lines = raw_text.strip().splitlines()
|
||||||
|
|
||||||
|
lines = [line.strip() for line in lines if not line.strip().startswith("#")]
|
||||||
|
|
||||||
|
method, url = lines[0].split(" ")
|
||||||
|
|
||||||
|
raw_headers = []
|
||||||
|
raw_body = []
|
||||||
|
is_body = False
|
||||||
|
|
||||||
|
for line in lines[1:]:
|
||||||
|
if not line.strip():
|
||||||
|
is_body = True
|
||||||
|
continue
|
||||||
|
if not is_body:
|
||||||
|
raw_headers.append(line)
|
||||||
|
else:
|
||||||
|
raw_body.append(line)
|
||||||
|
|
||||||
|
return HttpFileRequest(
|
||||||
|
method=method,
|
||||||
|
url=url,
|
||||||
|
headers=extract_headers(raw_headers),
|
||||||
|
body=parse_body("\n".join(raw_body)),
|
||||||
|
name=get_name(raw_text)
|
||||||
|
)
|
@ -58,6 +58,8 @@ install_requires =
|
|||||||
importlib-metadata>=1.4.0; python_version<"3.8"
|
importlib-metadata>=1.4.0; python_version<"3.8"
|
||||||
rich>=9.10.0
|
rich>=9.10.0
|
||||||
colorama>=0.2.4; sys_platform=="win32"
|
colorama>=0.2.4; sys_platform=="win32"
|
||||||
|
jsonpath_ng
|
||||||
|
lxml
|
||||||
python_requires = >=3.7
|
python_requires = >=3.7
|
||||||
|
|
||||||
|
|
||||||
|
569
tests/test_http_parser.py
Normal file
569
tests/test_http_parser.py
Normal file
@ -0,0 +1,569 @@
|
|||||||
|
import pytest
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from httpie.http_parser import (
|
||||||
|
split_requests,
|
||||||
|
replace_dependencies,
|
||||||
|
get_name,
|
||||||
|
replace_global,
|
||||||
|
extract_headers,
|
||||||
|
parse_body,
|
||||||
|
parse_single_request,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_whitespace(text):
|
||||||
|
"""Removes excessive newlines and spaces for consistent comparison."""
|
||||||
|
return "\n".join(line.rstrip() for line in text.splitlines()).strip()
|
||||||
|
|
||||||
|
|
||||||
|
# TESTS FOR split_requests -->> REQ_002
|
||||||
|
|
||||||
|
def test_split_requests():
|
||||||
|
# Test case: Multiple HTTP requests
|
||||||
|
http_file = """### Request 1
|
||||||
|
GET /users
|
||||||
|
|
||||||
|
### Request 2
|
||||||
|
POST /users
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{"name": "John"}"""
|
||||||
|
expected_output = [
|
||||||
|
"### Request 1\nGET /users",
|
||||||
|
"### Request 2\nPOST /users\nContent-Type: application/json\n\n{\"name\": \"John\"}"
|
||||||
|
]
|
||||||
|
assert list(map(normalize_whitespace, split_requests(http_file))) == list(
|
||||||
|
map(normalize_whitespace, expected_output)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_split_single_request():
|
||||||
|
"""
|
||||||
|
This test ensures that a single HTTP request with a '###' header is correctly parsed
|
||||||
|
without any unexpected modifications.
|
||||||
|
"""
|
||||||
|
http_file = """### Only Request
|
||||||
|
GET /status"""
|
||||||
|
expected_output = ["### Only Request\nGET /status"]
|
||||||
|
assert list(map(normalize_whitespace, split_requests(http_file))) == list(
|
||||||
|
map(normalize_whitespace, expected_output)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_split_empty_file():
|
||||||
|
"""
|
||||||
|
This test checks if an empty input correctly returns an empty list,
|
||||||
|
ensuring there are no errors when handling empty strings.
|
||||||
|
"""
|
||||||
|
assert split_requests("") == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_split_request_no_body():
|
||||||
|
"""
|
||||||
|
This test verifies that requests with no body (only headers and method)
|
||||||
|
are parsed correctly without adding unnecessary spaces or newlines.
|
||||||
|
"""
|
||||||
|
http_file = """### No Body Request
|
||||||
|
GET /ping"""
|
||||||
|
expected_output = ["### No Body Request\nGET /ping"]
|
||||||
|
assert list(map(normalize_whitespace, split_requests(http_file))) == list(
|
||||||
|
map(normalize_whitespace, expected_output)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_split_request_with_extra_newlines():
|
||||||
|
"""
|
||||||
|
This test ensures that the function correctly handles requests that
|
||||||
|
contain extra blank lines while preserving necessary formatting.
|
||||||
|
"""
|
||||||
|
http_file = """### Request 1
|
||||||
|
|
||||||
|
GET /data
|
||||||
|
|
||||||
|
|
||||||
|
### Request 2
|
||||||
|
|
||||||
|
POST /submit
|
||||||
|
|
||||||
|
{"key": "value"}
|
||||||
|
"""
|
||||||
|
expected_output = [
|
||||||
|
"### Request 1\nGET /data", # Normalized extra newline
|
||||||
|
"### Request 2\nPOST /submit\n\n{\"key\": \"value\"}" # Normalized newlines inside request
|
||||||
|
]
|
||||||
|
assert list(map(normalize_whitespace, split_requests(http_file))) == list(
|
||||||
|
map(normalize_whitespace, expected_output)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_split_request_without_header():
|
||||||
|
"""
|
||||||
|
This test ensures that requests without a '###' header are ignored and
|
||||||
|
do not cause the function to fail. The function should return an empty list
|
||||||
|
in such cases.
|
||||||
|
"""
|
||||||
|
http_file = """GET /withoutHeader"""
|
||||||
|
expected_output = [] # No '###' header means no valid requests should be returned
|
||||||
|
assert split_requests(http_file) == expected_output
|
||||||
|
|
||||||
|
|
||||||
|
# TESTS FOR get_dependencies -->> REQ_007
|
||||||
|
|
||||||
|
def test_replace_dependencies_no_placeholders():
|
||||||
|
"""
|
||||||
|
This test verifies that if a request does not contain any {{placeholders}},
|
||||||
|
the function correctly doesn't change anything.
|
||||||
|
"""
|
||||||
|
raw_request = """GET /users"""
|
||||||
|
assert replace_dependencies(raw_request, None) == """GET /users"""
|
||||||
|
|
||||||
|
|
||||||
|
def test_replace_dependencies_invalid_dependency():
|
||||||
|
"""
|
||||||
|
This test ensures that if the request references a dependency that is
|
||||||
|
not in the provided possible_names list, the function correctly raises an ValueError.
|
||||||
|
"""
|
||||||
|
raw_request = """DELETE /items/{{InvalidRequest}}"""
|
||||||
|
responses = {"Request1": None, "Request2": None}
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
replace_dependencies(raw_request, responses)
|
||||||
|
|
||||||
|
|
||||||
|
def test_replace_dependencies_Req_single():
|
||||||
|
"""
|
||||||
|
This test checks that a single valid dependency is correctly extracted
|
||||||
|
from a request and returned in a list.
|
||||||
|
"""
|
||||||
|
raw_request = """GET /update/{{Request1.request.headers.id}}"""
|
||||||
|
|
||||||
|
url = "https://api.example.com"
|
||||||
|
request = requests.Request('GET', url)
|
||||||
|
response = None
|
||||||
|
|
||||||
|
responses = {"Request1": [request, response]}
|
||||||
|
request.headers["id"] = str(1)
|
||||||
|
|
||||||
|
assert replace_dependencies(raw_request, responses) == """GET /update/1"""
|
||||||
|
|
||||||
|
|
||||||
|
def test_replace_dependencies_PreReq_single():
|
||||||
|
"""
|
||||||
|
This test checks that a single valid dependency is correctly extracted
|
||||||
|
from a PreparedRequest and returned in a list.
|
||||||
|
"""
|
||||||
|
raw_request = """GET /update/{{Request1.request.headers.id}}"""
|
||||||
|
|
||||||
|
url = "https://api.example.com"
|
||||||
|
session = requests.Session()
|
||||||
|
request = requests.Request('GET', url)
|
||||||
|
prepared_request = session.prepare_request(request)
|
||||||
|
response = None
|
||||||
|
|
||||||
|
responses = {"Request1": [prepared_request, response]}
|
||||||
|
prepared_request.headers["id"] = str(1)
|
||||||
|
|
||||||
|
assert replace_dependencies(raw_request, responses) == """GET /update/1"""
|
||||||
|
|
||||||
|
|
||||||
|
def test_replace_multiple_dependencies():
|
||||||
|
"""
|
||||||
|
This test verifies that multiple dependencies are correctly identified
|
||||||
|
and replaced in the request.
|
||||||
|
"""
|
||||||
|
raw_request = """GET /update/{{Request1.request.headers.id}}/{{Request1.request.headers.name}}"""
|
||||||
|
|
||||||
|
url = "https://api.example.com"
|
||||||
|
request = requests.Request('GET', url)
|
||||||
|
response = None
|
||||||
|
|
||||||
|
responses = {"Request1": [request, response]}
|
||||||
|
request.headers["id"] = str(1)
|
||||||
|
request.headers["name"] = "Jack"
|
||||||
|
|
||||||
|
assert replace_dependencies(raw_request, responses) == """GET /update/1/Jack"""
|
||||||
|
|
||||||
|
|
||||||
|
def test_replace_dependencies_empty_request():
|
||||||
|
"""
|
||||||
|
This test checks that an empty request string returns None
|
||||||
|
since there are no placeholders.
|
||||||
|
"""
|
||||||
|
raw_request = ""
|
||||||
|
assert replace_dependencies(raw_request, None) == ""
|
||||||
|
|
||||||
|
# TESTS FOR get_name --> REQ_003
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_name_with_hash_comment():
|
||||||
|
"""
|
||||||
|
Ensures that get_name correctly extracts a request name
|
||||||
|
when defined with '#' as a comment.
|
||||||
|
"""
|
||||||
|
raw_request = """# @name Request1
|
||||||
|
GET /users"""
|
||||||
|
expected_output = "Request1"
|
||||||
|
assert get_name(raw_request) == expected_output
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_name_with_double_slash_comment():
|
||||||
|
"""
|
||||||
|
Ensures that get_name correctly extracts a request name
|
||||||
|
when defined with '//' as a comment.
|
||||||
|
"""
|
||||||
|
raw_request = """// @name GetUser
|
||||||
|
GET /users/{id}"""
|
||||||
|
expected_output = "GetUser"
|
||||||
|
assert get_name(raw_request) == expected_output
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_name_no_name():
|
||||||
|
"""
|
||||||
|
Ensures that if no '@name' is present, get_name returns None.
|
||||||
|
"""
|
||||||
|
raw_request = """GET /users"""
|
||||||
|
assert get_name(raw_request) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_name_multiple_names():
|
||||||
|
"""
|
||||||
|
Ensures that if multiple '@name' occurrences exist,
|
||||||
|
the function returns None to indicate an error.
|
||||||
|
"""
|
||||||
|
raw_request = """# @name FirstName
|
||||||
|
GET /users
|
||||||
|
# @name SecondName
|
||||||
|
POST /users"""
|
||||||
|
assert get_name(raw_request) is None # Multiple names should result in None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_name_with_extra_whitespace():
|
||||||
|
"""
|
||||||
|
Ensures that extra spaces around @name do not affect the extracted name.
|
||||||
|
"""
|
||||||
|
raw_request = """ # @name MyRequest
|
||||||
|
GET /data"""
|
||||||
|
expected_output = "MyRequest"
|
||||||
|
assert get_name(raw_request) == expected_output
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_name_without_request():
|
||||||
|
"""
|
||||||
|
Ensures that a request with only an @name definition still correctly extracts the name.
|
||||||
|
"""
|
||||||
|
raw_request = """// @name LoneRequest"""
|
||||||
|
expected_output = "LoneRequest"
|
||||||
|
assert get_name(raw_request) == expected_output
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_name_inline_invalid():
|
||||||
|
"""
|
||||||
|
Ensures that @name only works when it starts a line,
|
||||||
|
and does not extract names from inline comments.
|
||||||
|
"""
|
||||||
|
raw_request = """GET /users # @name InlineName"""
|
||||||
|
assert get_name(raw_request) is None # Inline @name should not be detected
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_name_mixed_comment_styles():
|
||||||
|
"""
|
||||||
|
Ensures that if multiple valid @name comments exist,
|
||||||
|
the function returns None to indicate an error.
|
||||||
|
"""
|
||||||
|
raw_request = """# @name FirstRequest
|
||||||
|
// @name SecondRequest
|
||||||
|
GET /items"""
|
||||||
|
assert get_name(raw_request) is None
|
||||||
|
|
||||||
|
|
||||||
|
# TESTS FOR replace_global --> REQ_005
|
||||||
|
|
||||||
|
def test_replace_global_no_definitions():
|
||||||
|
"""
|
||||||
|
Ensures that if no global variable definitions are present,
|
||||||
|
the file contents remain unchanged.
|
||||||
|
"""
|
||||||
|
raw_contents = "GET /users/{{id}}"
|
||||||
|
expected_output = raw_contents # No replacement should occur
|
||||||
|
assert replace_global(raw_contents) == expected_output
|
||||||
|
|
||||||
|
|
||||||
|
def test_replace_global_single_variable():
|
||||||
|
"""
|
||||||
|
Ensures that a single global variable definition is correctly used to replace
|
||||||
|
all its corresponding placeholders in the file.
|
||||||
|
"""
|
||||||
|
raw_contents = """@host=example.com
|
||||||
|
GET http://{{host}}/users"""
|
||||||
|
expected_output = """@host=example.com
|
||||||
|
GET http://example.com/users"""
|
||||||
|
assert replace_global(raw_contents) == expected_output
|
||||||
|
|
||||||
|
|
||||||
|
def test_replace_global_multiple_variables():
|
||||||
|
"""
|
||||||
|
Ensures that multiple global variable definitions are correctly used to replace
|
||||||
|
their corresponding placeholders in the file.
|
||||||
|
"""
|
||||||
|
raw_contents = """@host=example.com
|
||||||
|
@port=8080
|
||||||
|
GET http://{{host}}:{{port}}/users"""
|
||||||
|
expected_output = """@host=example.com
|
||||||
|
@port=8080
|
||||||
|
GET http://example.com:8080/users"""
|
||||||
|
assert replace_global(raw_contents) == expected_output
|
||||||
|
|
||||||
|
|
||||||
|
def test_replace_global_multiple_occurrences():
|
||||||
|
"""
|
||||||
|
Ensures that if a variable appears multiple times in the file,
|
||||||
|
all occurrences are replaced.
|
||||||
|
"""
|
||||||
|
raw_contents = """@name=Test
|
||||||
|
GET /api?param={{name}}&other={{name}}"""
|
||||||
|
expected_output = """@name=Test
|
||||||
|
GET /api?param=Test&other=Test"""
|
||||||
|
assert replace_global(raw_contents) == expected_output
|
||||||
|
|
||||||
|
|
||||||
|
def test_replace_global_value_with_spaces():
|
||||||
|
"""
|
||||||
|
Ensures that global variable definitions with spaces in their values are handled correctly.
|
||||||
|
"""
|
||||||
|
raw_contents = """@greeting=Hello World
|
||||||
|
GET /message?text={{greeting}}"""
|
||||||
|
expected_output = """@greeting=Hello World
|
||||||
|
GET /message?text=Hello World"""
|
||||||
|
assert replace_global(raw_contents) == expected_output
|
||||||
|
|
||||||
|
|
||||||
|
def test_replace_global_definition_without_placeholder():
|
||||||
|
"""
|
||||||
|
Ensures that if a global variable is defined but its placeholder is not present,
|
||||||
|
the file remains unchanged.
|
||||||
|
"""
|
||||||
|
raw_contents = """@unused=Value
|
||||||
|
GET /info"""
|
||||||
|
expected_output = raw_contents # No replacement should occur
|
||||||
|
assert replace_global(raw_contents) == expected_output
|
||||||
|
|
||||||
|
|
||||||
|
# TESTS FOR extract_headers --> REQ_003
|
||||||
|
|
||||||
|
def test_extract_headers_empty():
|
||||||
|
"""
|
||||||
|
Test 1: Empty list should return an empty dictionary.
|
||||||
|
"""
|
||||||
|
raw_text = []
|
||||||
|
expected = {}
|
||||||
|
assert extract_headers(raw_text) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_headers_only_empty_lines():
|
||||||
|
"""
|
||||||
|
Test 2: Lines that are empty or only whitespace should be ignored.
|
||||||
|
"""
|
||||||
|
raw_text = ["", " ", "\t"]
|
||||||
|
expected = {}
|
||||||
|
assert extract_headers(raw_text) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_headers_single_header():
|
||||||
|
"""
|
||||||
|
Test 3: A single valid header line.
|
||||||
|
"""
|
||||||
|
raw_text = ["Content-Type: application/json"]
|
||||||
|
expected = {"Content-Type": "application/json"}
|
||||||
|
assert extract_headers(raw_text) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_headers_multiple_headers():
|
||||||
|
"""
|
||||||
|
Test 4: Multiple header lines should be parsed into a dictionary.
|
||||||
|
"""
|
||||||
|
raw_text = [
|
||||||
|
"Content-Type: application/json",
|
||||||
|
"Authorization: Bearer token123"
|
||||||
|
]
|
||||||
|
expected = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Bearer token123"
|
||||||
|
}
|
||||||
|
assert extract_headers(raw_text) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_headers_line_without_colon():
|
||||||
|
"""
|
||||||
|
Test 5: Lines without a colon should be ignored.
|
||||||
|
"""
|
||||||
|
raw_text = [
|
||||||
|
"This is not a header",
|
||||||
|
"Content-Length: 123"
|
||||||
|
]
|
||||||
|
expected = {"Content-Length": "123"}
|
||||||
|
assert extract_headers(raw_text) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_headers_extra_spaces():
|
||||||
|
"""
|
||||||
|
Test 6: Extra whitespace around header names and values should be trimmed.
|
||||||
|
"""
|
||||||
|
raw_text = [
|
||||||
|
" Accept : text/html "
|
||||||
|
]
|
||||||
|
expected = {"Accept": "text/html"}
|
||||||
|
assert extract_headers(raw_text) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_headers_multiple_colons():
|
||||||
|
"""
|
||||||
|
Test 7: Only the first colon should be used to split the header name and value.
|
||||||
|
"""
|
||||||
|
raw_text = [
|
||||||
|
"Custom-Header: value:with:colons"
|
||||||
|
]
|
||||||
|
expected = {"Custom-Header": "value:with:colons"}
|
||||||
|
assert extract_headers(raw_text) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_headers_duplicate_headers():
|
||||||
|
"""
|
||||||
|
Test 8: If a header appears more than once, the last occurrence should overwrite previous ones.
|
||||||
|
"""
|
||||||
|
raw_text = [
|
||||||
|
"X-Header: one",
|
||||||
|
"X-Header: two"
|
||||||
|
]
|
||||||
|
expected = {"X-Header": "two"}
|
||||||
|
assert extract_headers(raw_text) == expected
|
||||||
|
|
||||||
|
|
||||||
|
# TESTS FOR parse_body -->> REQ_002
|
||||||
|
# TODO: create tests after function definition is done
|
||||||
|
|
||||||
|
|
||||||
|
# TESTS FOR parse_single_request -->> REQ_002
|
||||||
|
|
||||||
|
def test_parse_single_request_minimal():
|
||||||
|
"""
|
||||||
|
A minimal HTTP request that only contains the request line (method and URL).
|
||||||
|
Expected:
|
||||||
|
- method and URL are parsed correctly.
|
||||||
|
- headers is an empty dict.
|
||||||
|
- body is empty (after processing by parse_body).
|
||||||
|
- dependencies is an empty dict.
|
||||||
|
- name is None (since no @name comment exists).
|
||||||
|
"""
|
||||||
|
raw_text = "GET http://example.com"
|
||||||
|
result = parse_single_request(raw_text)
|
||||||
|
assert result.method == "GET"
|
||||||
|
assert result.url == "http://example.com"
|
||||||
|
assert result.headers == {}
|
||||||
|
expected_body = parse_body("")
|
||||||
|
assert result.body == expected_body
|
||||||
|
assert result.name is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_single_request_with_headers_and_body():
|
||||||
|
"""
|
||||||
|
Tests a request that includes a request line, headers, and a body.
|
||||||
|
Expected:
|
||||||
|
- Correctly parsed method and URL.
|
||||||
|
- Headers are extracted into a dictionary.
|
||||||
|
- The body is passed through parse_body and matches the expected output.
|
||||||
|
- No @name is defined, so name is None.
|
||||||
|
"""
|
||||||
|
raw_text = """POST http://example.com/api
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer token
|
||||||
|
|
||||||
|
{
|
||||||
|
"key": "value"
|
||||||
|
}"""
|
||||||
|
result = parse_single_request(raw_text)
|
||||||
|
assert result.method == "POST"
|
||||||
|
assert result.url == "http://example.com/api"
|
||||||
|
assert result.headers == {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Bearer token"
|
||||||
|
}
|
||||||
|
expected_body = parse_body("{\n \"key\": \"value\"\n}")
|
||||||
|
assert result.body == expected_body
|
||||||
|
assert result.name is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_single_request_with_name():
|
||||||
|
"""
|
||||||
|
Tests a request that includes a @name comment.
|
||||||
|
The @name line is removed from the parsed lines (since lines starting with '#' are filtered out)
|
||||||
|
but get_name is still applied on the original raw text.
|
||||||
|
Expected:
|
||||||
|
- name is extracted as defined by get_name.
|
||||||
|
- Other fields (method, URL, headers, body) are parsed normally.
|
||||||
|
"""
|
||||||
|
raw_text = """# @name MyTestRequest
|
||||||
|
GET http://example.com
|
||||||
|
Content-Type: text/plain
|
||||||
|
|
||||||
|
Hello, world!
|
||||||
|
"""
|
||||||
|
result = parse_single_request(raw_text)
|
||||||
|
assert result.method == "GET"
|
||||||
|
assert result.url == "http://example.com"
|
||||||
|
assert result.headers == {"Content-Type": "text/plain"}
|
||||||
|
expected_body = parse_body("Hello, world!")
|
||||||
|
assert result.body == expected_body
|
||||||
|
assert result.name == "MyTestRequest"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_single_request_extra_blank_lines():
|
||||||
|
"""
|
||||||
|
Tests that multiple blank lines (which trigger the switch from headers to body)
|
||||||
|
are handled properly.
|
||||||
|
Expected:
|
||||||
|
- The request line is parsed.
|
||||||
|
- Headers are extracted before the first blank line.
|
||||||
|
- Everything after the blank lines is treated as the body.
|
||||||
|
"""
|
||||||
|
raw_text = """PUT http://example.com/update
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
|
||||||
|
Line one of the body.
|
||||||
|
Line two of the body.
|
||||||
|
"""
|
||||||
|
result = parse_single_request(raw_text)
|
||||||
|
assert result.method == "PUT"
|
||||||
|
assert result.url == "http://example.com/update"
|
||||||
|
assert result.headers == {"Accept": "application/json"}
|
||||||
|
expected_body = parse_body("Line one of the body.\nLine two of the body.")
|
||||||
|
assert result.body == expected_body
|
||||||
|
assert result.name is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_single_request_ignore_comments():
|
||||||
|
"""
|
||||||
|
Tests that lines starting with '#' (comments) are removed from the parsed headers.
|
||||||
|
Note: Even if the @name line is a comment, get_name is called on the original raw text,
|
||||||
|
so it may still extract a name.
|
||||||
|
Expected:
|
||||||
|
- Headers only include valid header lines.
|
||||||
|
- The @name is still extracted if present in the raw text.
|
||||||
|
"""
|
||||||
|
raw_text = """# @name CommentedRequest
|
||||||
|
GET http://example.com/data
|
||||||
|
# This comment should be ignored
|
||||||
|
Content-Length: 123
|
||||||
|
|
||||||
|
"""
|
||||||
|
result = parse_single_request(raw_text)
|
||||||
|
assert result.method == "GET"
|
||||||
|
assert result.url == "http://example.com/data"
|
||||||
|
assert result.headers == {"Content-Length": "123"}
|
||||||
|
expected_body = parse_body("")
|
||||||
|
assert result.body == expected_body
|
||||||
|
assert result.name == "CommentedRequest"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main()
|
Loading…
Reference in New Issue
Block a user