Loading libraries/robotframework-httpctrl/src/HttpCtrl/utils/__init__.py 0 → 100755 +9 −0 Viewed Changes for libraries/robotframework-httpctrl/src/HttpCtrl/utils/__init__.py: 9 added lines, 0 removed lines. Original line number Diff line number Diff line """ HttpCtrl library provides HTTP/HTTPS client and server API to Robot Framework to make REST API testing easy. Authors: Andrei Novikov Date: 2018-2022 Copyright: The 3-Clause BSD License """ libraries/robotframework-httpctrl/src/HttpCtrl/utils/logger.py 0 → 100755 +37 −0 Viewed Changes for libraries/robotframework-httpctrl/src/HttpCtrl/utils/logger.py: 37 added lines, 0 removed lines. Original line number Diff line number Diff line """ HttpCtrl library provides HTTP/HTTPS client and server API to Robot Framework to make REST API testing easy. Authors: Andrei Novikov Date: 2018-2022 Copyright: The 3-Clause BSD License """ from robot.api import logger class LoggerAssistant: __MAX_BODY_SIZE_TO_LOG = 512 @staticmethod def set_body_size(body_size): if body_size is None: logger.info("Disable the limit for the body size to log.") else: logger.info("Set the limit for the body size to log: '%d'." % body_size) LoggerAssistant.__MAX_BODY_SIZE_TO_LOG = body_size @staticmethod def get_body(body): if body is not None: if (LoggerAssistant.__MAX_BODY_SIZE_TO_LOG is None) or (len(body) < LoggerAssistant.__MAX_BODY_SIZE_TO_LOG): return body else: return "%s...\n...\n<display only the first '%d' from '%d' symbols>" % (body[:LoggerAssistant.__MAX_BODY_SIZE_TO_LOG], LoggerAssistant.__MAX_BODY_SIZE_TO_LOG, len(body)) return "" libraries/robotframework-httpctrl/src/HttpCtrl/utils/singleton.py 0 → 100755 +17 −0 Viewed Changes for libraries/robotframework-httpctrl/src/HttpCtrl/utils/singleton.py: 17 added lines, 0 removed lines. Original line number Diff line number Diff line """ HttpCtrl library provides HTTP/HTTPS client and server API to Robot Framework to make REST API testing easy. Authors: Andrei Novikov Date: 2018-2022 Copyright: The 3-Clause BSD License """ class Singleton(type): _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) return cls._instances[cls] No newline at end of file libraries/robotframework-httpctrl/src/HttpCtrl/__init__.py 0 → 100755 +1615 −0 Viewed Changes for libraries/robotframework-httpctrl/src/HttpCtrl/__init__.py: 1615 added lines, 0 removed lines. Original line number Diff line number Diff line """ HttpCtrl library provides HTTP/HTTPS client and server API to Robot Framework to make REST API testing easy. Authors: Andrei Novikov Date: 2018-2022 Copyright: The 3-Clause BSD License """ import datetime import http.client import json import threading from robot.api import logger from HttpCtrl.utils.logger import LoggerAssistant from HttpCtrl.internal_messages import IgnoreRequest from HttpCtrl.http_server import HttpServer from HttpCtrl.http_stub import HttpStubContainer, HttpStubCriteria from HttpCtrl.request_storage import RequestStorage from HttpCtrl.response_storage import ResponseStorage from HttpCtrl.response import Response class Client: """ HTTP/HTTPS Client library that provides comprehensive interface to Robot Framework to control HTTP/HTTPS client. See other HttpCtrl libraries: - HttpCtrl.Server_ - HTTP Server API for testing where easy-controlled HTTP server is required. - HttpCtrl.Json_ - Json related API for testing where work with Json message is required. - HttpCtrl.Logging_ - Logging related API to configure the logging system that is used by HttpCtrl library. .. _HttpCtrl.Server: server.html .. _HttpCtrl.Json: json.html .. _HttpCtrl.Logging: logging.html Example how to send GET request to obtain origin IP address and check that response is not empty: .. code:: robotframework *** Settings *** Library HttpCtrl.Client Library HttpCtrl.Json *** Test Cases *** Get Origin Address Initialize Client www.httpbin.org Send HTTP Request GET /ip ${response status}= Get Response Status ${response body}= Get Response Body ${expected status}= Convert To Integer 200 Should Be Equal ${response status} ${expected status} ${origin}= Get Json Value From String ${response body} origin Should Not Be Empty ${origin} Example how to sent PATCH request using HTTPS: .. code:: robotframework *** Settings *** Library HttpCtrl.Client Library HttpCtrl.Json *** Test Cases *** Send HTTPS PATCH Request Initialize Client www.httpbin.org ${body}= Set Variable { "volume": 77, "mute": false } Send HTTPS Request PATCH /patch ${body} ${response status}= Get Response Status ${response body}= Get Response Body ${response body}= Decode Bytes To String ${response body} UTF-8 ${expected status}= Convert To Integer 200 Should Be Equal ${response status} ${expected status} ${volume}= Get Json Value From String ${response body} json/volume Should Be Equal ${volume} ${77} ${mute}= Get Json Value From String ${response body} json/mute Should Be Equal ${mute} ${False} """ def __init__(self): self.__server_host = None self.__server_port = None self.__client_host = None self.__client_port = None self.__request_headers = {} self.__response_guard = threading.Lock() self.__response_status = None self.__response_message = None self.__response_body_filename = None self.__response_body = None self.__response_headers = None self.__event_queue = threading.Condition() self.__async_queue = {} def initialize_client(self, server_host, server_port=None, client_host=None, client_port=0): """ Initialize client using host and port of a server which will be used for communication. `server_host` [in] (string): Host of a server that is going to be used for communication by a client. `server_port` [in] (string|integer): Port of a server that is going to be used for communication by a client. Optional argument. `client_host` [in] (string): Host of a client (source) that is used to bind. Optional argument. `client_port` [in] (string|integer): Port of a client (source) that is used to bind. Optional argument. Example when server is located on a machine with address 192.168.0.1 and port 8000: +-------------------+-------------+------+ | Initialize Client | 192.168.0.1 | 8000 | +-------------------+-------------+------+ Example when server is located on a machine with IPv6 address 0000:0000:0000:0000:0000:0000:0000:0001 and port 8000: +-------------------+-----------------------------------------+------+ | Initialize Client | 0000:0000:0000:0000:0000:0000:0000:0001 | 8000 | +-------------------+-----------------------------------------+------+ .. code:: text Initialize Client 192.168.0.1 8000 Example when your server has name: +-------------------+-----------------+ | Initialize Client | www.httpbin.org | +-------------------+-----------------+ .. code:: text Initialize Client www.httpbin.org Example when a client is bind to the specific address 192.168.0.1 and port 8001: +-------------------+-------------+------+-------------+------+ | Initialize Client | 192.168.0.5 | 8000 | 192.168.0.1 | 8001 | +-------------------+-------------+------+-------------+------+ .. code:: text Initialize Client 192.168.0.5 8000 192.168.0.1 8001 Example when a client is bind to the specific address only 192.168.0.1 (without port): +-------------------+-------------+------+-------------+ | Initialize Client | 192.168.0.5 | 8000 | 192.168.0.1 | +-------------------+-------------+------+-------------+ .. code:: text Initialize Client 192.168.0.5 8000 192.168.0.1 """ self.__server_host = server_host self.__server_port = server_port or "" self.__client_host = client_host self.__client_port = client_port def __get_source_address(self): if self.__client_host is None: return None return self.__client_host, int(self.__client_port) def __send(self, connection_type, method, url, body): if self.__server_host is None or self.__server_port is None: raise AssertionError("Client is not initialized (host and port are empty).") endpoint = "%s:%s" % (self.__server_host, str(self.__server_port)) source_address = self.__get_source_address() logger.info("Connect to the server (type: '%s', endpoint: '%s')" % (connection_type, endpoint)) if connection_type == 'http': connection = http.client.HTTPConnection(endpoint, source_address=source_address) elif connection_type == 'https': connection = http.client.HTTPSConnection(endpoint, source_address=source_address) else: raise AssertionError("Internal error of the client, please report to " "'https://github.com/annoviko/robotframework-httpctrl/issues'.") logger.info("Send request to the server (method: '%s', url: '%s')." % (method, url)) try: connection.request(method, url, body, self.__request_headers) except Exception as exception: logger.info("Impossible to send request to the server (reason: '%s')." % str(exception)) self.__request_headers = {} logger.info("Request (type: '%s', method '%s') was sent to '%s'." % (connection_type, method, endpoint)) logger.info("%s %s" % (method, url)) if body is not None: body_to_log = LoggerAssistant.get_body(body) logger.info("%s" % body_to_log) return connection def __read_body_to_file(self, server_response, filename): logger.info("Write body to file '%s'." % filename) default_chunk_size = 10000000 # 10 MByte with open(filename, "wb") as file_stream: while True: obtained_chunk = server_response.read(default_chunk_size) if not obtained_chunk: break file_stream.write(obtained_chunk) def __wait_response(self, connection, read_body_to_file): try: server_response = connection.getresponse() with self.__response_guard: self.__response_status = server_response.status self.__response_message = server_response.msg self.__response_headers = server_response.headers if read_body_to_file is None: self.__response_body_filename = None self.__response_body = server_response.read() else: self.__response_body_filename = read_body_to_file self.__response_body = None self.__read_body_to_file(server_response, read_body_to_file) except Exception as exception: logger.info("Server has not provided response to the request (reason: %s)." % str(exception)) finally: connection.close() def __wait_response_async(self, connection, read_body_to_file): try: server_response = connection.getresponse() with self.__event_queue: if read_body_to_file is None: response_body_file = None response_body = server_response.read() else: response_body_file = read_body_to_file response_body = None self.__read_body_to_file(server_response, read_body_to_file) headers = server_response.getheaders() headers_dict = {} if headers is not None: for key, value in headers: headers_dict[key] = value response_instance = Response(server_response.status, server_response.reason, response_body, response_body_file, headers_dict) self.__async_queue[connection] = response_instance self.__event_queue.notify_all() except Exception as exception: logger.info("Server has not provided response to the request (reason: %s)." % str(exception)) finally: connection.close() def __send_request(self, connection_type, method, url, body, read_body_to_file): connection = self.__send(connection_type, method, url, body) self.__wait_response(connection, read_body_to_file) def __sent_request_async(self, connection_type, method, url, body, read_body_to_file): connection = self.__send(connection_type, method, url, body) wait_thread = threading.Thread(target=self.__wait_response_async, args=(connection, read_body_to_file)) wait_thread.daemon = True wait_thread.start() return connection def send_http_request(self, method, url, body=None, resp_body_to_file=None): """ Send HTTP request with specified parameters. This function is blocked until server replies or timeout connection. `method` [in] (string): Method that is used to send request (GET, POST, PUT, DELETE, etc., see: RFC 7231, RFC 5789). `url` [in] (string): Path to the resource, for example, in case address www.httpbin.org/ip - '/ip' is an path. `body` [in] (string): Body of the request. `resp_body_to_file` [in] (string): Path to file where response body should be written. By default is `None` - response body is writing in RAM. It is useful to write response body into a file when it is expected to be big enough to keep it in the memory. Example where GET request is sent to server: +-------------------+-----+-----+ | Send HTTP Request | GET | /ip | +-------------------+-----+-----+ .. code:: text Send HTTP Request GET /ip Example where POST request is sent with specific body: +-------------------+------+-------+-------------------------------+ | Send HTTP Request | POST | /post | { "message": "Hello World!" } | +-------------------+------+-------+-------------------------------+ .. code:: text ${body}= Set Variable { "message": "Hello World!" } Send HTTP Request POST /post ${body} Example where GET request is sent and where response body is written into a file: +-------------------+-----+--------------------+-----------------------------------+ | Send HTTP Request | GET | /download_big_file | resp_body_to_file=big_archive.tar | +-------------------+-----+--------------------+-----------------------------------+ .. code:: text Send HTTP Request GET /download_big_file resp_body_to_file=big_archive.tar """ self.__send_request('http', method, url, body, resp_body_to_file) def send_http_request_async(self, method, url, body=None, resp_body_to_file=None): """ Send HTTP request with specified parameters asynchronously. Non-blocking function to send request that waits for reply using separate thread. Return connection object that is used as a key to get asynchronous response using function 'Get Async Response'. `method` [in] (string): Method that is used to send request (GET, POST, PUT, DELETE, etc., see: RFC 7231, RFC 5789). `url` [in] (string): Path to the resource, for example, in case address www.httpbin.org/ip - '/ip' is an path. `body` [in] (string): Body of the request. `resp_body_to_file` [in] (string): Path to file where response body should be written. By default is `None` - response body is writing in RAM. It is useful to write response body into a file when it is expected to be big enough to keep it in the memory. Example where PUT request is sent with specific body: +----------------+-------------------------+-----+------+---------------+ | ${connection}= | Send HTTP Request Async | PUT | /put | Hello Server! | +----------------+-------------------------+-----+------+---------------+ .. code:: text ${connection}= Send HTTP Request Async PUT /put Hello Server! Example where GET request is sent and where response body is written into a file: +----------------+-------------------------+-----+--------------------+-----------------------------------+ | ${connection}= | Send HTTP Request Async | GET | /download_big_file | resp_body_to_file=big_archive.tar | +----------------+-------------------------+-----+--------------------+-----------------------------------+ .. code:: text ${connection}= Send HTTP Request Async GET /download_big_file resp_body_to_file=big_archive.tar """ return self.__sent_request_async('http', method, url, body, resp_body_to_file) def send_https_request(self, method, url, body=None, resp_body_to_file=None): """ Send HTTPS request with specified parameters. `method` [in] (string): Method that is used to send request (GET, POST, DELETE, etc., see: RFC 7231, RFC 5789). `url` [in] (string): Path to the resource, for example, in case address www.httpbin.org/ip - '/ip' is an path. `body` [in] (string): Body of the request. `resp_body_to_file` [in] (string): Path to file where response body should be written. By default is `None` - response body is writing in RAM. It is useful to write response body into a file when it is expected to be big enough to keep it in the memory. Example where PATCH request to update parameters: +--------------------+-------+--------+---------------------------------+ | Send HTTPS Request | PATCH | /patch | { "volume": 77, "mute": false } | +--------------------+-------+--------+---------------------------------+ .. code:: text ${body}= Set Variable { "volume": 77, "mute": false } Send HTTPS Request PATCH /patch ${body} """ self.__send_request('https', method, url, body, resp_body_to_file) def send_https_request_async(self, method, url, body=None, resp_body_to_file=None): """ Send HTTPS request with specified parameters asynchronously. Non-blocking function to send request that waits for reply using separate thread. Return connection object that is used as a key to get asynchronous response using function 'Get Async Response'. `method` [in] (string): Method that is used to send request (GET, POST, DELETE, etc., see: RFC 7231, RFC 5789). `url` [in] (string): Path to the resource, for example, in case address www.httpbin.org/ip - '/ip' is an path. `body` [in] (string): Body of the request. `resp_body_to_file` [in] (string): Path to file where response body should be written. By default is `None` - response body is writing in RAM. It is useful to write response body into a file when it is expected to be big enough to keep it in the memory. Example where DELETE request is sent with specific body: +----------------+--------------------------+--------+---------+ | ${connection}= | Send HTTPS Request Async | DELETE | /delete | +----------------+--------------------------+--------+---------+ .. code:: text ${connection}= Send HTTPS Request Async DELETE /delete """ return self.__sent_request_async('https', method, url, body, resp_body_to_file) def set_request_header(self, key, value): """ Set HTTP header for request that is going to be sent. Should be called before 'Send HTTP Request' or 'Send HTTPS Request'. `key` [in] (string): Header name that should be used in the request (be aware of case-sensitive headers). `value` [in] (string): Value that corresponds to specified header. Example where several specific headers 'Content-Type' and 'Some Header' are set to request: +--------------------+------------------+-------------------+ | Set Request Header | Important-Header | important-value | +--------------------+------------------+-------------------+ | Set Request Header | Some-Header | some-value-for-it | +--------------------+------------------+-------------------+ .. code:: text Set Request Header Important-Header important-value Set Request Header Some-Header some-value-for-it """ self.__request_headers[key] = value def get_response_status(self): """ Return response code as an integer value. This method should be called once after 'Send HTTP Request' or 'Send HTTPS Request'. It returns None, in case of attempt to get response code more then once or if 'Send HTTP Request' or 'Send HTTPS Request' is not called before. Example how to get response code: +---------------------+---------------------+ | ${response status}= | Get Response Status | +---------------------+---------------------+ .. code:: text ${response status}= Get Response Status """ with self.__response_guard: status = self.__response_status self.__response_status = None return status def get_response_message(self): with self.__response_guard: message = self.__response_message self.__response_message = None return message def get_response_headers(self): """ Return response headers as a dictionary. This method should be called once after 'Send HTTP Request' or 'Send HTTPS Request'. It returns None, in case of attempt to get response code more then once or if 'Send HTTP Request' or 'Send HTTPS Request' is not called before. Example how to get response code: +----------------------+----------------------+ | ${response headers}= | Get Response Headers | +----------------------+----------------------+ .. code:: text ${response headers}= Get Response Headers """ with self.__response_guard: headers = self.__response_headers self.__response_headers = None return headers def get_response_body(self): """ Return response body as a byte array. This method should be called once after 'Send HTTP Request' or 'Send HTTPS Request'. It returns None, in case of attempt to get response code more then once or if 'Send HTTP Request' or 'Send HTTPS Request' is not called before. Example how to get response code: +-------------------+-------------------+ | ${response body}= | Get Response Body | +-------------------+-------------------+ .. code:: text ${response body}= Get Response Body """ with self.__response_guard: body = None if self.__response_body is not None: body = self.__response_body elif self.__response_body_filename is not None: with open(self.__response_body_filename) as file_stream: body = file_stream.read() self.__response_body_filename = None self.__response_body = None return body def get_async_response(self, connection, timeout=0): """ Return response as an object for the specified connection. This method should be called once after 'Send HTTP Request Async' or 'Send HTTPS Request Async'. It returns None if there is no response for the specified connection. `connection` [in] (object): Connection for that response should be obtained. `timeout` [in] (int): Period of time in seconds to obtain response (by default is 0). Example how to get response object: +--------------+--------------------+ | ${response}= | Get Async Response | +--------------+--------------------+ Example how to try to get response object during 10 seconds: +--------------+--------------------+----+ | ${response}= | Get Async Response | 10 | +--------------+--------------------+----+ .. code:: text ${connection}= Send HTTP Async Request POST /post Hello Server! ${response}= Get Async Response ${connection} 5 """ with self.__event_queue: start_time = datetime.datetime.now() end_time = start_time while (end_time - start_time).total_seconds() < int(timeout): if connection in self.__async_queue: return self.__async_queue.pop(connection) def predicate(): return connection in self.__async_queue self.__event_queue.wait_for(predicate, int(timeout)) end_time = datetime.datetime.now() return self.__async_queue.pop(connection, None) def get_status_from_response(self, response : Response): """ Return response status as an integer value from the specified response object that was obtained by function 'Get Async Response'. Return 'None' if response object is None. Example how to get response status from a response object: +---------------------+--------------------------+-------------+ | ${response status}= | Get Status From Response | ${response} | +---------------------+--------------------------+-------------+ .. code:: text ${connection}= Send HTTP Async Request GET /get # Some other actions ... ${response}= Get Async Response ${connection} 5 ${response status}= Get Status From Response ${response} """ if response is None: logger.error("Impossible to get status from 'None' response object.") return None return response.get_status() def get_reason_from_response(self, response : Response): """ Return response reason as a string from the specified response object that was obtained by function 'Get Async Response'. For example, response code and reason are '200 OK', in this case 'OK' is going to be returned by this function. Example how to get response reason from a response object: +---------------------+--------------------------+-------------+ | ${response reason}= | Get Reason From Response | ${response} | +---------------------+--------------------------+-------------+ .. code:: text ${connection}= Send HTTP Async Request GET /get # Some other actions ... ${response}= Get Async Response ${connection} 5 ${response reason}= Get Reason From Response ${response} """ if response is None: logger.error("Impossible to get reason from 'None' response object.") return None return response.get_reason() def get_headers_from_response(self, response : Response) -> dict: """ Return response headers as a dictionary from the specified response object that was obtained by function 'Get Async Response'. Return 'None' if response object is None. Example how to get response headers from a response object: +----------------------+---------------------------+-------------+ | ${response headers}= | Get Headers From Response | ${response} | +----------------------+---------------------------+-------------+ .. code:: text ${connection}= Send HTTP Async Request GET /get # Some other actions ... ${response}= Get Async Response ${connection} 5 ${response headers}= Get Headers From Response ${response} """ if response is None: logger.error("Impossible to get headers from 'None' response object.") return None return response.get_headers() def get_body_from_response(self, response : Response): """ Return response body as a byte array from the specified response object that was obtained by function 'Get Async Response'. Return 'None' if response object is None. Example how to get response code from a response object: +-------------------+------------------------+-------------+ | ${response body}= | Get Body From Response | ${response} | +-------------------+------------------------+-------------+ .. code:: text ${connection}= Send HTTP Async Request GET /get # Some other actions ... ${response}= Get Async Response ${connection} 5 ${response body}= Get Body From Response ${response} """ if response is None: logger.error("Impossible to get body from 'None' response object.") return None return response.get_body() class Server: """ HTTP Server library that provides comprehensive interface to Robot Framework to control HTTP server. See other HttpCtrl libraries: - HttpCtrl.Client_ - HTTP/HTTP Client API for testing where easy-controlled HTTP/HTTPS client is required. - HttpCtrl.Json_ - Json related API for testing where work with Json message is required. - HttpCtrl.Logging_ - Logging related API to configure the logging system that is used by HttpCtrl library. .. _HttpCtrl.Client: client.html .. _HttpCtrl.Json: json.html .. _HttpCtrl.Logging: logging.html Here is an example of receiving POST request. In this example HTTP client sends POST request to HTTP server. HTTP server receives it and checks incoming request for correctness. .. code:: robotframework *** Settings *** Library String Library HttpCtrl.Client Library HttpCtrl.Server Test Setup Initialize HTTP Client And Server Test Teardown Terminate HTTP Server *** Test Cases *** Receive And Reply To POST ${request body}= Set Variable { "method": "POST" } Send HTTP Request Async POST /post ${request body} Wait For Request Reply By 200 ${method}= Get Request Method ${url}= Get Request Url ${body}= Get Request Body ${body}= Decode Bytes To String ${body} UTF-8 Should Be Equal ${method} POST Should Be Equal ${url} /post Should Be Equal ${body} ${request body} *** Keywords *** Initialize HTTP Client And Server Initialize Client 127.0.0.1 8000 Start Server 127.0.0.1 8000 Terminate HTTP Server Stop Server In case of requirement to use IPv6 the keyword `Initialize HTTP Client And Server` might be the following: .. code:: robotframework *** Keywords *** Initialize HTTP Client And Server Initialize Client 0000:0000:0000:0000:0000:0000:0000:0001 8000 Start Server 0000:0000:0000:0000:0000:0000:0000:0001 8000 There is an example where server stubs are using. Sever stub is a pre-defined function that is used by the server to reply automatically to a request that satisfies a user specific HTTP criteria. .. code:: robotframework *** Settings *** Library String Library HttpCtrl.Client Library HttpCtrl.Server Test Setup Initialize HTTP Client And Server Test Teardown Terminate HTTP Server *** Test Cases *** Set Signle Stub And Send Request # Set server stub to reply automatically to POST /api/v1/post Set Stub Reply POST /api/v1/post 200 Post Message # Send HTTP request to the server Send HTTP Request POST /api/v1/post # Check that the client receives pre-defined values by the stub ${status}= Get Response Status ${body}= Get Response Body ${body}= Decode Bytes To String ${body} UTF-8 Should Be Equal ${status} ${200} Should Be Equal ${body} Post Message # Check that the server receives a single request ${count}= Get Stub Count POST /api/v1/post Should Be Equal ${count} ${1} *** Keywords *** Initialize HTTP Client And Server Initialize Client 127.0.0.1 8000 Start Server 127.0.0.1 8000 Terminate HTTP Server Stop Server """ def __init__(self): self.__response_headers = {} self.__request = None self.__server = None self.__thread = None def __del__(self): self.stop_server() def start_server(self, host, port): """ Start HTTP server on specific address and port. Server should be closed when it is not required, for example, when test is over. In case of double call of \`Start Server\`, the previous will be stopped and only then the next one HTTP server will be started. `host` [in] (string): Address that will be used by HTTP server to listen. `port` [in] (string): Port that will be used by HTTP server to listen. Example how to initialize server: +--------------+-----------+------+ | Start Server | 127.0.0.1 | 8000 | +--------------+-----------+------+ .. code:: text Start Server 127.0.0.1 8000 It is a good practice to start server and stop it using 'Test Setup' and 'Test Teardown', for example: .. code:: robotframework *** Settings *** Library HttpCtrl.Server Test Setup Initialize HTTP Server Test Teardown Terminate HTTP Server *** Test Cases *** HTTP Server Based Test Wait For Request Reply By 200 # Check incoming request *** Keywords *** Initialize HTTP Server Start Server 127.0.0.1 8000 Terminate HTTP Server Stop Server """ self.stop_server() logger.info("Prepare HTTP server '%s:%s' and thread to serve it." % (host, port)) self.__server = HttpServer(host, int(port)) self.__thread = threading.Thread(target=self.__server.start, args=()) self.__thread.start() # In case of start-stop, stop may be finished before server start and ir will be impossible to join thread. self.__server.wait_run_state() def stop_server(self): """ Stop HTTP server if it has been started. This function should be called if server has been started. Example how to stop server: +-------------+ | Stop Server | +-------------+ .. code:: text Stop Server It is a good practice to start server and stop it using `Test Setup` and `Test Teardown` - see example for \`Start Server\`. """ if self.__server is not None: self.__server.stop() self.__thread.join() self.__response_headers = {} self.__request = None ResponseStorage().clear() RequestStorage().clear() HttpStubContainer().clear() self.__server = None self.__thread = None logger.info("HTTP server is stopped.") def wait_for_request(self, timeout=5): """ Command to server to wait incoming request. This call is blocked until HTTP request arrives. Basically server receives all requests after \`Start Server\` and places them to internal queue. When test call function \`Wait For Request\` it checks the queue and if it is not empty returns the first request in the queue. If the queue is empty then function waits when the server receives request and place it to the queue. There is default time period '5 seconds' to wait request and this waiting time can be changed. If during wait time the request is not received then timeout error occurs. `timeout` [in] (int): Period of time in seconds when a request should be received by HTTP server. Example how to wait request. +------------------+ | Wait For Request | +------------------+ .. code:: text Wait For Request Example how to wait request during 2 seconds. +------------------+---+ | Wait For Request | 2 | +------------------+---+ .. code:: text Wait For Request 2 """ self.__request = RequestStorage().pop(int(timeout)) if self.__request is None: raise AssertionError("Timeout: request was not received.") logger.info("Request is received: %s" % self.__request) def wait_for_no_request(self, timeout=5.0): """ Command to server to wait for no incoming request during specific time. This call is blocked until HTTP request arrives or timeout. Basically server receives all requests after \`Start Server\` and places them to internal queue. When test call function \`Wait For No Request\` it checks the queue and if it is not empty returns throws exception. Otherwise it waits for request during 'timeout' seconds. If during this time request is received then exception is thrown. `timeout` [in] (int): Period of time in seconds when requests should not be received by HTTP server. Example how to wait for lack of requests. +---------------------+ | Wait For No Request | +---------------------+ .. code:: text Wait For No Request Example how to wait for lack of requests during 10 seconds. +---------------------+----+ | Wait For No Request | 10 | +---------------------+----+ .. code:: text Wait For No Request 10 """ self.__request = RequestStorage().pop(int(timeout)) if self.__request is not None: raise AssertionError("Request was received: %s." % self.__request) logger.info("Request is not received.") def wait_and_ignore_request(self): """ Command to server to wait incoming request and ignore it by closing connection. This call is blocked until HTTP request arrives. Basically server receives all requests after \`Start Server\` and places them to internal queue. When test call function \`Wait And Ignore Request\` it checks the queue and if it is not empty returns the first request in the queue is ignore and connection is closed. If the queue is empty then function waits when the server receives request and place it to the queue. Example how to wait and ignore request. +-------------------------+ | Wait And Ignore Request | +-------------------------+ .. code:: text Wait And Ignore Request """ self.wait_for_request() ResponseStorage().push(IgnoreRequest()) logger.info("Request is ignored by closing connection.") def set_stub_reply(self, method, url, status, body=None): """ Sets stub reply for HTTP(S) server. This function sets a server stub to reply automatically by a specific response to a specific request. When the stub is used to reply, then corresponding statistic is incremented (see \`Get Stub Count\`). `method` [in] (string): Request method that is used to handle by server stub (GET, POST, DELETE, etc., see: RFC 7231, RFC 5789). `url` [in] (string): Path to the resource that is used by server stub, for example, in case address www.httpbin.org/ip - '/ip' is an path. `status` [in] (int|string): HTTP status code for response that is used by server stub. `body` [in] (string|bytes): Response body that is used by server stub. Example how to set stub to reply automatically to request `POST` `/api/v1/request` by status `200`. +----------------+------+-----------------+-----+ | Set Stub Reply | POST | /api/v1/request | 200 | +----------------+------+-----------------+-----+ .. code:: text Set Stub Reply POST /api/v1/request 200 Example how to set stub to reply automatically using a specific body. +----------------+------+-----------------+-----+--------------------------+ | Set Stub Reply | POST | /api/v1/request | 202 | Request has been handled | +----------------+------+-----------------+-----+--------------------------+ .. code:: text Set Stub Reply POST /api/v1/request 200 Request has been handled """ if self.__server is None: message_error = "Impossible to set server stub reply (reason: 'server is not created')." raise AssertionError(message_error) criteria = HttpStubCriteria(method=method, url=url) response = Response(int(status), None, json.dumps(body), None, None) HttpStubContainer().add(criteria, response) def get_stub_count(self, method, url): """ Returns server stub statistic that defines how many time the stub was used by server to reply. `method` [in] (string): Request method that is used to handle by server stub (GET, POST, DELETE, etc., see: RFC 7231, RFC 5789). `url` [in] (string): Path to the resource that is used by server stub, for example, in case address www.httpbin.org/ip - '/ip' is an path. Example how to get server stub statistic for request with `POST` method and URL `/api/v2/request`. +----------------+------+-----------------+ | Get Stub Count | POST | /api/v2/request | +----------------+------+-----------------+ .. code:: text Get Stub Count POST /api/v2/request Example how to get server stub statistic for request with `GET` method and URL `/get` +----------------+------+-----+ | Get Stub Count | GET | /get | +----------------+------+-----+ .. code:: text Get Stub Count GET /get """ if self.__server is None: message_error = "Impossible to get server stub statistic (reason: 'server is not created')." raise AssertionError(message_error) criteria = HttpStubCriteria(method=method, url=url) return HttpStubContainer().count(criteria) def get_request_source_address(self): """ Returns source address (client address) of received request as string value. This function should be called after \`Wait For Request\`, otherwise None is returned. Example how to obtain source address of incoming request: +----------------------------+ | Get Request Source Address | +----------------------------+ .. code:: text ${source address}= Get Request Source Address """ return self.__request.get_source_address() def get_request_source_port(self): """ Returns source port (client port) of received request as string value. This function should be called after \`Wait For Request\`, otherwise None is returned. Example how to obtain source port of incoming request: +-------------------------+ | Get Request Source Port | +-------------------------+ .. code:: text ${source port}= Get Request Source Port """ return str(self.__request.get_source_port()) def get_request_source_port_as_integer(self): """ Returns source port (client port) of received request as integer value. This function should be called after \`Wait For Request\`, otherwise None is returned. Example how to obtain source port of incoming request: +------------------------------------+ | Get Request Source Port As Integer | +------------------------------------+ .. code:: text ${source port}= Get Request Source Port As Integer """ return self.__request.get_source_port() def get_request_method(self): """ Returns method of received request as a string. This function should be called after \`Wait For Request\`, otherwise None is returned. Example how to obtain method of incoming request: +--------------------+ | Get Request Method | +--------------------+ .. code:: text Get Request Method """ return self.__request.get_method() def get_request_body(self): """ Returns body of received request as a string. This function should be called after \`Wait For Request\`, otherwise None is returned. Example how to obtain body of incoming request: +------------------+ | Get Request Body | +------------------+ .. code:: text Get Request Body """ return self.__request.get_body() def get_request_headers(self): """ Returns headers of received request as a dictionary. This function should be called after \`Wait For Request\`, otherwise None is returned. Example how to obtain headers of incoming request: +---------------------+ | Get Request Headers | +---------------------+ .. code:: text Get Request Headers """ return self.__request.get_headers() def get_request_url(self): """ Returns URL of received request as a string. This function should be called after \`Wait For Request\`, otherwise None is returned. Example how to obtain URL of incoming request: +-----------------+ | Get Request Url | +-----------------+ .. code:: text Get Request Url """ return self.__request.get_url() def set_reply_header(self, key, value): """ Set or insert new (if it does not exist yet) header to HTTP response. To send response itself function \`Reply By\` is used. `key` [in] (string): HTTP header name. `value` [in] (string): HTTP header value. Example how to set header for HTTP response: +------------------+--------+----------------+ | Set Reply Header | Origin | 127.0.0.1:8000 | +------------------+--------+----------------+ .. code:: text Set Reply Header Origin 127.0.0.1:8000 Example how to set several headers for HTTP response: +------------------+-------------+----------------+ | Set Reply Header | Origin | 127.0.0.1:8001 | +------------------+-------------+----------------+ | Set Reply Header | City-Source | St.-Petersburg | +------------------+-------------+----------------+ .. code:: text Set Reply Header Origin 127.0.0.1:8000 Set Reply Header City-Source St.-Petersburg """ self.__response_headers[key] = value def reply_by(self, status, body=None): """ Send response using specified HTTP code and body. This function should be called after \`Wait For Request\`. `status` [in] (string): HTTP status code for response. `body` [in] (string|bytes): Body that should contain response. Example how to reply by 204 (No Content) to incoming request: +----------+-----+ | Reply By | 204 | +----------+-----+ .. code:: text Reply By 204 Example how to reply 200 (OK) with body to incoming request: +----------+-----+--------------------------+ | Reply By | 200 | { "status": "accepted" } | +----------+-----+--------------------------+ .. code:: text ${response body}= Set Variable { "status": "accepted" } Reply By 204 ${response body} Example how to reply with a body represented by a sequence of bytes: ..code:: text Wait For Request ${body bytes}= Evaluate bytes((0x0a, 0x12, 0x0a)) Reply By 200 ${body bytes} """ response = Response(int(status), None, body, None, self.__response_headers) ResponseStorage().push(response) class Json: """ Json library provide comprehensive interface to Robot Framework to work with JSON structures that are actively used for Internet communication nowadays. See other HttpCtrl libraries: - HttpCtrl.Client_ - HTTP/HTTP Client API for testing where easy-controlled HTTP/HTTPS client is required. - HttpCtrl.Server_ - HTTP Server API for testing where easy-controlled HTTP server is required. - HttpCtrl.Logging_ - Logging related API to configure the logging system that is used by HttpCtrl library. .. _HttpCtrl.Client: client.html .. _HttpCtrl.Server: server.html .. _HttpCtrl.Logging: logging.html Example where Json values are updated in a string and then read from it: .. code:: robotframework *** Settings *** Library HttpCtrl.Client Library HttpCtrl.Json *** Test Cases *** Write And Read Json Nesting Value ${json template}= Catenate ... { ... "book": { ... "title": "St Petersburg: A Cultural History", ... "author": "Solomon Volkov", ... "price": 0, ... "currency": "" ... } ... } ${catalog}= Set Json Value In String ${json template} book/price ${500} ${catalog}= Set Json Value In String ${catalog} book/currency RUB ${title}= Get Json Value From String ${catalog} book/title ${price}= Get Json Value From String ${catalog} book/price ${currency}= Get Json Value From String ${catalog} book/currency Here is an another example where Json array's values are updated and then read from it: .. code:: robotframework *** Settings *** Library HttpCtrl.Client Library HttpCtrl.Json *** Test Cases *** Write And Read Json Array Value ${json template}= Catenate ... { ... "array": [ ... "red", "green", "blue", "yellow" ... ] ... } ${colors}= Set Json Value In String ${json template} array/3 white ${red}= Get Json Value From String ${colors} array/0 ${green}= Get Json Value From String ${colors} array/1 ${blue}= Get Json Value From String ${colors} array/2 ${white}= Get Json Value From String ${colors} array/3 """ @staticmethod def get_json_value_from_string(json_string, path): """ Return value from Json that is represented by string. Be aware, returned value's type corresponds to its type in Json. `json_string` [in] (string): Json that is represented by string. `path` [in] (string): Path to Json node value. Example how to obtain Json value: +-----------+----------------------------+------------------------------------+------------+ | ${value}= | Get Json Value From String | { "book": { "title": "Unknown" } } | book/title | +-----------+----------------------------+------------------------------------+------------+ .. code:: text ${json}= Catenate ... { ... "book": { ... "title": "Unknown" ... } ... } ${value}= Get Json Value From String ${json} book/title """ json_content = json.loads(json_string) keys = path.split('/') current_element = json_content for key in keys: if isinstance(current_element, list): key = int(key) current_element = current_element[key] return current_element @staticmethod def set_json_value_in_string(json_string, path, value): """ Set value in Json that is represented by string. Be aware type of updated value should correspond to its type in Json, otherwise type will be changed with new value. `json_string` [in] (string): Json that is represented by string. `path` [in] (string): Path to Json node value. `value` [in] (any): New value for the Json node. Example how to set Json value: +-----------+--------------------------+------------------------------------+------------+--------+ | ${value}= | Set Json Value In String | { "book": { "title": "Unknown" } } | book/title | "Math" | +-----------+--------------------------+------------------------------------+------------+--------+ .. code:: text ${json}= Catenate ... { ... "book": { ... "title": "Unknown" ... } ... } ${value}= Set Json Value In String ${json} book/title "Math" Example how to set value in Json array node: +-----------+--------------------------+--------------------------------------+---------+---------+ | ${value}= | Set Json Value In String | { "array": [ "one", "two", "ten" ] } | array/2 | "three" | +-----------+--------------------------+--------------------------------------+---------+---------+ .. code:: text ${json}= Catenate ... { ... "array": [ ... "one", "two", "ten" ... ] ... } ${value}= Set Json Value In String ${json} array/2 "three" """ json_content = json.loads(json_string) keys = path.split('/') current_element = json_content for key in keys: if key == keys[-1]: if isinstance(current_element, list): key = int(key) current_element[key] = value else: if isinstance(current_element, list): key = int(key) current_element = current_element[key] return json.dumps(json_content) class Logging: """ Logging library provide functionality to configure the logging system that is used by HttpCtrl library. See other HttpCtrl libraries: - HttpCtrl.Client_ - HTTP/HTTP Client API for testing where easy-controlled HTTP/HTTPS client is required. - HttpCtrl.Server_ - HTTP Server API for testing where easy-controlled HTTP server is required. - HttpCtrl.Json_ - Json related API for testing where work with Json message is required. .. _HttpCtrl.Client: client.html .. _HttpCtrl.Server: server.html .. _HttpCtrl.Json: json.html """ @staticmethod def set_body_size_limit_to_log(body_size): """ Set body (HTTP request/response) size that is allowed to log. By default the library logs `512` symbols of the body. If the limit should be removed then `${None}` value can be provided to the function. The logging body limit protects test logs to be too large if tests use big data for testing. Example how to set the logging body limit to 1024 symbols: +----------------------------+------+ | Set Body Size Limit To Log | 1024 | +----------------------------+------+ .. code:: text Set Body Size Limit To Log 1024 Example how to remove the logging body limit: +----------------------------+---------+ | Set Body Size Limit To Log | ${None} | +----------------------------+---------+ .. code:: text Set Body Size Limit To Log ${None} """ LoggerAssistant.set_body_size(body_size) libraries/robotframework-httpctrl/src/HttpCtrl/http_handler.py 0 → 100755 +125 −0 Viewed Changes for libraries/robotframework-httpctrl/src/HttpCtrl/http_handler.py: 125 added lines, 0 removed lines. Original line number Diff line number Diff line """ HttpCtrl library provides HTTP/HTTPS client and server API to Robot Framework to make REST API testing easy. Authors: Andrei Novikov Date: 2018-2022 Copyright: The 3-Clause BSD License """ from http.server import SimpleHTTPRequestHandler from robot.api import logger from HttpCtrl.internal_messages import TerminationRequest, IgnoreRequest from HttpCtrl.request import Request from HttpCtrl.request_storage import RequestStorage from HttpCtrl.response_storage import ResponseStorage from HttpCtrl.http_stub import HttpStubContainer, HttpStubCriteria class HttpHandler(SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): self.server_version = "HttpCtrl.Server/" self.sys_version = "" SimpleHTTPRequestHandler.__init__(self, *args, **kwargs) def do_GET(self): self.__default_handler('GET') def do_POST(self): self.__default_handler('POST') def do_PUT(self): self.__default_handler('PUT') def do_OPTIONS(self): self.__default_handler('OPTIONS') def do_HEAD(self): self.__default_handler('HEAD') def do_PATCH(self): self.__default_handler('PATCH') def do_DELETE(self): self.__default_handler('DELETE') def log_message(self, format, *args): return def log_error(self, format, *args): return def log_request(self, code='-', size='-'): return def __extract_body(self): body_length = int(self.headers.get('Content-Length', 0)) if body_length > 0: return self.rfile.read(body_length) return None def __default_handler(self, method): host, port = self.client_address[:2] body = self.__extract_body() logger.info("'%s' request is received from '%s:%s'." % (method, host, port)) stub = HttpStubContainer().get(HttpStubCriteria(method=method, url=self.path), body) if stub is not None: response = stub.response request = Request(host, port, method, self.path, self.headers, body) RequestStorage().push(request) else: request = Request(host, port, method, self.path, self.headers, body) RequestStorage().push(request) response = ResponseStorage().pop() if isinstance(response, TerminationRequest) or isinstance(response, IgnoreRequest): return try: self.__send_response(response) except Exception as exception: logger.info("Response was not sent to client due to reason: '%s'." % str(exception)) def __send_response(self, response): if response is None: logger.error("Response is not provided for incoming request.") return self.send_response(response.get_status()) headers = response.get_headers() if headers is not None: for key, value in headers.items(): self.send_header(key, value) body = response.get_body() if body is not None: if isinstance(body, str): body = body.encode("utf-8") self.send_header('Content-Length', str(len(body))) self.end_headers() if body is not None: self.wfile.write(body) No newline at end of file Loading
libraries/robotframework-httpctrl/src/HttpCtrl/utils/__init__.py 0 → 100755 +9 −0 Viewed Changes for libraries/robotframework-httpctrl/src/HttpCtrl/utils/__init__.py: 9 added lines, 0 removed lines. Original line number Diff line number Diff line """ HttpCtrl library provides HTTP/HTTPS client and server API to Robot Framework to make REST API testing easy. Authors: Andrei Novikov Date: 2018-2022 Copyright: The 3-Clause BSD License """
libraries/robotframework-httpctrl/src/HttpCtrl/utils/logger.py 0 → 100755 +37 −0 Viewed Changes for libraries/robotframework-httpctrl/src/HttpCtrl/utils/logger.py: 37 added lines, 0 removed lines. Original line number Diff line number Diff line """ HttpCtrl library provides HTTP/HTTPS client and server API to Robot Framework to make REST API testing easy. Authors: Andrei Novikov Date: 2018-2022 Copyright: The 3-Clause BSD License """ from robot.api import logger class LoggerAssistant: __MAX_BODY_SIZE_TO_LOG = 512 @staticmethod def set_body_size(body_size): if body_size is None: logger.info("Disable the limit for the body size to log.") else: logger.info("Set the limit for the body size to log: '%d'." % body_size) LoggerAssistant.__MAX_BODY_SIZE_TO_LOG = body_size @staticmethod def get_body(body): if body is not None: if (LoggerAssistant.__MAX_BODY_SIZE_TO_LOG is None) or (len(body) < LoggerAssistant.__MAX_BODY_SIZE_TO_LOG): return body else: return "%s...\n...\n<display only the first '%d' from '%d' symbols>" % (body[:LoggerAssistant.__MAX_BODY_SIZE_TO_LOG], LoggerAssistant.__MAX_BODY_SIZE_TO_LOG, len(body)) return ""
libraries/robotframework-httpctrl/src/HttpCtrl/utils/singleton.py 0 → 100755 +17 −0 Viewed Changes for libraries/robotframework-httpctrl/src/HttpCtrl/utils/singleton.py: 17 added lines, 0 removed lines. Original line number Diff line number Diff line """ HttpCtrl library provides HTTP/HTTPS client and server API to Robot Framework to make REST API testing easy. Authors: Andrei Novikov Date: 2018-2022 Copyright: The 3-Clause BSD License """ class Singleton(type): _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) return cls._instances[cls] No newline at end of file
libraries/robotframework-httpctrl/src/HttpCtrl/__init__.py 0 → 100755 +1615 −0 Viewed Changes for libraries/robotframework-httpctrl/src/HttpCtrl/__init__.py: 1615 added lines, 0 removed lines. Original line number Diff line number Diff line """ HttpCtrl library provides HTTP/HTTPS client and server API to Robot Framework to make REST API testing easy. Authors: Andrei Novikov Date: 2018-2022 Copyright: The 3-Clause BSD License """ import datetime import http.client import json import threading from robot.api import logger from HttpCtrl.utils.logger import LoggerAssistant from HttpCtrl.internal_messages import IgnoreRequest from HttpCtrl.http_server import HttpServer from HttpCtrl.http_stub import HttpStubContainer, HttpStubCriteria from HttpCtrl.request_storage import RequestStorage from HttpCtrl.response_storage import ResponseStorage from HttpCtrl.response import Response class Client: """ HTTP/HTTPS Client library that provides comprehensive interface to Robot Framework to control HTTP/HTTPS client. See other HttpCtrl libraries: - HttpCtrl.Server_ - HTTP Server API for testing where easy-controlled HTTP server is required. - HttpCtrl.Json_ - Json related API for testing where work with Json message is required. - HttpCtrl.Logging_ - Logging related API to configure the logging system that is used by HttpCtrl library. .. _HttpCtrl.Server: server.html .. _HttpCtrl.Json: json.html .. _HttpCtrl.Logging: logging.html Example how to send GET request to obtain origin IP address and check that response is not empty: .. code:: robotframework *** Settings *** Library HttpCtrl.Client Library HttpCtrl.Json *** Test Cases *** Get Origin Address Initialize Client www.httpbin.org Send HTTP Request GET /ip ${response status}= Get Response Status ${response body}= Get Response Body ${expected status}= Convert To Integer 200 Should Be Equal ${response status} ${expected status} ${origin}= Get Json Value From String ${response body} origin Should Not Be Empty ${origin} Example how to sent PATCH request using HTTPS: .. code:: robotframework *** Settings *** Library HttpCtrl.Client Library HttpCtrl.Json *** Test Cases *** Send HTTPS PATCH Request Initialize Client www.httpbin.org ${body}= Set Variable { "volume": 77, "mute": false } Send HTTPS Request PATCH /patch ${body} ${response status}= Get Response Status ${response body}= Get Response Body ${response body}= Decode Bytes To String ${response body} UTF-8 ${expected status}= Convert To Integer 200 Should Be Equal ${response status} ${expected status} ${volume}= Get Json Value From String ${response body} json/volume Should Be Equal ${volume} ${77} ${mute}= Get Json Value From String ${response body} json/mute Should Be Equal ${mute} ${False} """ def __init__(self): self.__server_host = None self.__server_port = None self.__client_host = None self.__client_port = None self.__request_headers = {} self.__response_guard = threading.Lock() self.__response_status = None self.__response_message = None self.__response_body_filename = None self.__response_body = None self.__response_headers = None self.__event_queue = threading.Condition() self.__async_queue = {} def initialize_client(self, server_host, server_port=None, client_host=None, client_port=0): """ Initialize client using host and port of a server which will be used for communication. `server_host` [in] (string): Host of a server that is going to be used for communication by a client. `server_port` [in] (string|integer): Port of a server that is going to be used for communication by a client. Optional argument. `client_host` [in] (string): Host of a client (source) that is used to bind. Optional argument. `client_port` [in] (string|integer): Port of a client (source) that is used to bind. Optional argument. Example when server is located on a machine with address 192.168.0.1 and port 8000: +-------------------+-------------+------+ | Initialize Client | 192.168.0.1 | 8000 | +-------------------+-------------+------+ Example when server is located on a machine with IPv6 address 0000:0000:0000:0000:0000:0000:0000:0001 and port 8000: +-------------------+-----------------------------------------+------+ | Initialize Client | 0000:0000:0000:0000:0000:0000:0000:0001 | 8000 | +-------------------+-----------------------------------------+------+ .. code:: text Initialize Client 192.168.0.1 8000 Example when your server has name: +-------------------+-----------------+ | Initialize Client | www.httpbin.org | +-------------------+-----------------+ .. code:: text Initialize Client www.httpbin.org Example when a client is bind to the specific address 192.168.0.1 and port 8001: +-------------------+-------------+------+-------------+------+ | Initialize Client | 192.168.0.5 | 8000 | 192.168.0.1 | 8001 | +-------------------+-------------+------+-------------+------+ .. code:: text Initialize Client 192.168.0.5 8000 192.168.0.1 8001 Example when a client is bind to the specific address only 192.168.0.1 (without port): +-------------------+-------------+------+-------------+ | Initialize Client | 192.168.0.5 | 8000 | 192.168.0.1 | +-------------------+-------------+------+-------------+ .. code:: text Initialize Client 192.168.0.5 8000 192.168.0.1 """ self.__server_host = server_host self.__server_port = server_port or "" self.__client_host = client_host self.__client_port = client_port def __get_source_address(self): if self.__client_host is None: return None return self.__client_host, int(self.__client_port) def __send(self, connection_type, method, url, body): if self.__server_host is None or self.__server_port is None: raise AssertionError("Client is not initialized (host and port are empty).") endpoint = "%s:%s" % (self.__server_host, str(self.__server_port)) source_address = self.__get_source_address() logger.info("Connect to the server (type: '%s', endpoint: '%s')" % (connection_type, endpoint)) if connection_type == 'http': connection = http.client.HTTPConnection(endpoint, source_address=source_address) elif connection_type == 'https': connection = http.client.HTTPSConnection(endpoint, source_address=source_address) else: raise AssertionError("Internal error of the client, please report to " "'https://github.com/annoviko/robotframework-httpctrl/issues'.") logger.info("Send request to the server (method: '%s', url: '%s')." % (method, url)) try: connection.request(method, url, body, self.__request_headers) except Exception as exception: logger.info("Impossible to send request to the server (reason: '%s')." % str(exception)) self.__request_headers = {} logger.info("Request (type: '%s', method '%s') was sent to '%s'." % (connection_type, method, endpoint)) logger.info("%s %s" % (method, url)) if body is not None: body_to_log = LoggerAssistant.get_body(body) logger.info("%s" % body_to_log) return connection def __read_body_to_file(self, server_response, filename): logger.info("Write body to file '%s'." % filename) default_chunk_size = 10000000 # 10 MByte with open(filename, "wb") as file_stream: while True: obtained_chunk = server_response.read(default_chunk_size) if not obtained_chunk: break file_stream.write(obtained_chunk) def __wait_response(self, connection, read_body_to_file): try: server_response = connection.getresponse() with self.__response_guard: self.__response_status = server_response.status self.__response_message = server_response.msg self.__response_headers = server_response.headers if read_body_to_file is None: self.__response_body_filename = None self.__response_body = server_response.read() else: self.__response_body_filename = read_body_to_file self.__response_body = None self.__read_body_to_file(server_response, read_body_to_file) except Exception as exception: logger.info("Server has not provided response to the request (reason: %s)." % str(exception)) finally: connection.close() def __wait_response_async(self, connection, read_body_to_file): try: server_response = connection.getresponse() with self.__event_queue: if read_body_to_file is None: response_body_file = None response_body = server_response.read() else: response_body_file = read_body_to_file response_body = None self.__read_body_to_file(server_response, read_body_to_file) headers = server_response.getheaders() headers_dict = {} if headers is not None: for key, value in headers: headers_dict[key] = value response_instance = Response(server_response.status, server_response.reason, response_body, response_body_file, headers_dict) self.__async_queue[connection] = response_instance self.__event_queue.notify_all() except Exception as exception: logger.info("Server has not provided response to the request (reason: %s)." % str(exception)) finally: connection.close() def __send_request(self, connection_type, method, url, body, read_body_to_file): connection = self.__send(connection_type, method, url, body) self.__wait_response(connection, read_body_to_file) def __sent_request_async(self, connection_type, method, url, body, read_body_to_file): connection = self.__send(connection_type, method, url, body) wait_thread = threading.Thread(target=self.__wait_response_async, args=(connection, read_body_to_file)) wait_thread.daemon = True wait_thread.start() return connection def send_http_request(self, method, url, body=None, resp_body_to_file=None): """ Send HTTP request with specified parameters. This function is blocked until server replies or timeout connection. `method` [in] (string): Method that is used to send request (GET, POST, PUT, DELETE, etc., see: RFC 7231, RFC 5789). `url` [in] (string): Path to the resource, for example, in case address www.httpbin.org/ip - '/ip' is an path. `body` [in] (string): Body of the request. `resp_body_to_file` [in] (string): Path to file where response body should be written. By default is `None` - response body is writing in RAM. It is useful to write response body into a file when it is expected to be big enough to keep it in the memory. Example where GET request is sent to server: +-------------------+-----+-----+ | Send HTTP Request | GET | /ip | +-------------------+-----+-----+ .. code:: text Send HTTP Request GET /ip Example where POST request is sent with specific body: +-------------------+------+-------+-------------------------------+ | Send HTTP Request | POST | /post | { "message": "Hello World!" } | +-------------------+------+-------+-------------------------------+ .. code:: text ${body}= Set Variable { "message": "Hello World!" } Send HTTP Request POST /post ${body} Example where GET request is sent and where response body is written into a file: +-------------------+-----+--------------------+-----------------------------------+ | Send HTTP Request | GET | /download_big_file | resp_body_to_file=big_archive.tar | +-------------------+-----+--------------------+-----------------------------------+ .. code:: text Send HTTP Request GET /download_big_file resp_body_to_file=big_archive.tar """ self.__send_request('http', method, url, body, resp_body_to_file) def send_http_request_async(self, method, url, body=None, resp_body_to_file=None): """ Send HTTP request with specified parameters asynchronously. Non-blocking function to send request that waits for reply using separate thread. Return connection object that is used as a key to get asynchronous response using function 'Get Async Response'. `method` [in] (string): Method that is used to send request (GET, POST, PUT, DELETE, etc., see: RFC 7231, RFC 5789). `url` [in] (string): Path to the resource, for example, in case address www.httpbin.org/ip - '/ip' is an path. `body` [in] (string): Body of the request. `resp_body_to_file` [in] (string): Path to file where response body should be written. By default is `None` - response body is writing in RAM. It is useful to write response body into a file when it is expected to be big enough to keep it in the memory. Example where PUT request is sent with specific body: +----------------+-------------------------+-----+------+---------------+ | ${connection}= | Send HTTP Request Async | PUT | /put | Hello Server! | +----------------+-------------------------+-----+------+---------------+ .. code:: text ${connection}= Send HTTP Request Async PUT /put Hello Server! Example where GET request is sent and where response body is written into a file: +----------------+-------------------------+-----+--------------------+-----------------------------------+ | ${connection}= | Send HTTP Request Async | GET | /download_big_file | resp_body_to_file=big_archive.tar | +----------------+-------------------------+-----+--------------------+-----------------------------------+ .. code:: text ${connection}= Send HTTP Request Async GET /download_big_file resp_body_to_file=big_archive.tar """ return self.__sent_request_async('http', method, url, body, resp_body_to_file) def send_https_request(self, method, url, body=None, resp_body_to_file=None): """ Send HTTPS request with specified parameters. `method` [in] (string): Method that is used to send request (GET, POST, DELETE, etc., see: RFC 7231, RFC 5789). `url` [in] (string): Path to the resource, for example, in case address www.httpbin.org/ip - '/ip' is an path. `body` [in] (string): Body of the request. `resp_body_to_file` [in] (string): Path to file where response body should be written. By default is `None` - response body is writing in RAM. It is useful to write response body into a file when it is expected to be big enough to keep it in the memory. Example where PATCH request to update parameters: +--------------------+-------+--------+---------------------------------+ | Send HTTPS Request | PATCH | /patch | { "volume": 77, "mute": false } | +--------------------+-------+--------+---------------------------------+ .. code:: text ${body}= Set Variable { "volume": 77, "mute": false } Send HTTPS Request PATCH /patch ${body} """ self.__send_request('https', method, url, body, resp_body_to_file) def send_https_request_async(self, method, url, body=None, resp_body_to_file=None): """ Send HTTPS request with specified parameters asynchronously. Non-blocking function to send request that waits for reply using separate thread. Return connection object that is used as a key to get asynchronous response using function 'Get Async Response'. `method` [in] (string): Method that is used to send request (GET, POST, DELETE, etc., see: RFC 7231, RFC 5789). `url` [in] (string): Path to the resource, for example, in case address www.httpbin.org/ip - '/ip' is an path. `body` [in] (string): Body of the request. `resp_body_to_file` [in] (string): Path to file where response body should be written. By default is `None` - response body is writing in RAM. It is useful to write response body into a file when it is expected to be big enough to keep it in the memory. Example where DELETE request is sent with specific body: +----------------+--------------------------+--------+---------+ | ${connection}= | Send HTTPS Request Async | DELETE | /delete | +----------------+--------------------------+--------+---------+ .. code:: text ${connection}= Send HTTPS Request Async DELETE /delete """ return self.__sent_request_async('https', method, url, body, resp_body_to_file) def set_request_header(self, key, value): """ Set HTTP header for request that is going to be sent. Should be called before 'Send HTTP Request' or 'Send HTTPS Request'. `key` [in] (string): Header name that should be used in the request (be aware of case-sensitive headers). `value` [in] (string): Value that corresponds to specified header. Example where several specific headers 'Content-Type' and 'Some Header' are set to request: +--------------------+------------------+-------------------+ | Set Request Header | Important-Header | important-value | +--------------------+------------------+-------------------+ | Set Request Header | Some-Header | some-value-for-it | +--------------------+------------------+-------------------+ .. code:: text Set Request Header Important-Header important-value Set Request Header Some-Header some-value-for-it """ self.__request_headers[key] = value def get_response_status(self): """ Return response code as an integer value. This method should be called once after 'Send HTTP Request' or 'Send HTTPS Request'. It returns None, in case of attempt to get response code more then once or if 'Send HTTP Request' or 'Send HTTPS Request' is not called before. Example how to get response code: +---------------------+---------------------+ | ${response status}= | Get Response Status | +---------------------+---------------------+ .. code:: text ${response status}= Get Response Status """ with self.__response_guard: status = self.__response_status self.__response_status = None return status def get_response_message(self): with self.__response_guard: message = self.__response_message self.__response_message = None return message def get_response_headers(self): """ Return response headers as a dictionary. This method should be called once after 'Send HTTP Request' or 'Send HTTPS Request'. It returns None, in case of attempt to get response code more then once or if 'Send HTTP Request' or 'Send HTTPS Request' is not called before. Example how to get response code: +----------------------+----------------------+ | ${response headers}= | Get Response Headers | +----------------------+----------------------+ .. code:: text ${response headers}= Get Response Headers """ with self.__response_guard: headers = self.__response_headers self.__response_headers = None return headers def get_response_body(self): """ Return response body as a byte array. This method should be called once after 'Send HTTP Request' or 'Send HTTPS Request'. It returns None, in case of attempt to get response code more then once or if 'Send HTTP Request' or 'Send HTTPS Request' is not called before. Example how to get response code: +-------------------+-------------------+ | ${response body}= | Get Response Body | +-------------------+-------------------+ .. code:: text ${response body}= Get Response Body """ with self.__response_guard: body = None if self.__response_body is not None: body = self.__response_body elif self.__response_body_filename is not None: with open(self.__response_body_filename) as file_stream: body = file_stream.read() self.__response_body_filename = None self.__response_body = None return body def get_async_response(self, connection, timeout=0): """ Return response as an object for the specified connection. This method should be called once after 'Send HTTP Request Async' or 'Send HTTPS Request Async'. It returns None if there is no response for the specified connection. `connection` [in] (object): Connection for that response should be obtained. `timeout` [in] (int): Period of time in seconds to obtain response (by default is 0). Example how to get response object: +--------------+--------------------+ | ${response}= | Get Async Response | +--------------+--------------------+ Example how to try to get response object during 10 seconds: +--------------+--------------------+----+ | ${response}= | Get Async Response | 10 | +--------------+--------------------+----+ .. code:: text ${connection}= Send HTTP Async Request POST /post Hello Server! ${response}= Get Async Response ${connection} 5 """ with self.__event_queue: start_time = datetime.datetime.now() end_time = start_time while (end_time - start_time).total_seconds() < int(timeout): if connection in self.__async_queue: return self.__async_queue.pop(connection) def predicate(): return connection in self.__async_queue self.__event_queue.wait_for(predicate, int(timeout)) end_time = datetime.datetime.now() return self.__async_queue.pop(connection, None) def get_status_from_response(self, response : Response): """ Return response status as an integer value from the specified response object that was obtained by function 'Get Async Response'. Return 'None' if response object is None. Example how to get response status from a response object: +---------------------+--------------------------+-------------+ | ${response status}= | Get Status From Response | ${response} | +---------------------+--------------------------+-------------+ .. code:: text ${connection}= Send HTTP Async Request GET /get # Some other actions ... ${response}= Get Async Response ${connection} 5 ${response status}= Get Status From Response ${response} """ if response is None: logger.error("Impossible to get status from 'None' response object.") return None return response.get_status() def get_reason_from_response(self, response : Response): """ Return response reason as a string from the specified response object that was obtained by function 'Get Async Response'. For example, response code and reason are '200 OK', in this case 'OK' is going to be returned by this function. Example how to get response reason from a response object: +---------------------+--------------------------+-------------+ | ${response reason}= | Get Reason From Response | ${response} | +---------------------+--------------------------+-------------+ .. code:: text ${connection}= Send HTTP Async Request GET /get # Some other actions ... ${response}= Get Async Response ${connection} 5 ${response reason}= Get Reason From Response ${response} """ if response is None: logger.error("Impossible to get reason from 'None' response object.") return None return response.get_reason() def get_headers_from_response(self, response : Response) -> dict: """ Return response headers as a dictionary from the specified response object that was obtained by function 'Get Async Response'. Return 'None' if response object is None. Example how to get response headers from a response object: +----------------------+---------------------------+-------------+ | ${response headers}= | Get Headers From Response | ${response} | +----------------------+---------------------------+-------------+ .. code:: text ${connection}= Send HTTP Async Request GET /get # Some other actions ... ${response}= Get Async Response ${connection} 5 ${response headers}= Get Headers From Response ${response} """ if response is None: logger.error("Impossible to get headers from 'None' response object.") return None return response.get_headers() def get_body_from_response(self, response : Response): """ Return response body as a byte array from the specified response object that was obtained by function 'Get Async Response'. Return 'None' if response object is None. Example how to get response code from a response object: +-------------------+------------------------+-------------+ | ${response body}= | Get Body From Response | ${response} | +-------------------+------------------------+-------------+ .. code:: text ${connection}= Send HTTP Async Request GET /get # Some other actions ... ${response}= Get Async Response ${connection} 5 ${response body}= Get Body From Response ${response} """ if response is None: logger.error("Impossible to get body from 'None' response object.") return None return response.get_body() class Server: """ HTTP Server library that provides comprehensive interface to Robot Framework to control HTTP server. See other HttpCtrl libraries: - HttpCtrl.Client_ - HTTP/HTTP Client API for testing where easy-controlled HTTP/HTTPS client is required. - HttpCtrl.Json_ - Json related API for testing where work with Json message is required. - HttpCtrl.Logging_ - Logging related API to configure the logging system that is used by HttpCtrl library. .. _HttpCtrl.Client: client.html .. _HttpCtrl.Json: json.html .. _HttpCtrl.Logging: logging.html Here is an example of receiving POST request. In this example HTTP client sends POST request to HTTP server. HTTP server receives it and checks incoming request for correctness. .. code:: robotframework *** Settings *** Library String Library HttpCtrl.Client Library HttpCtrl.Server Test Setup Initialize HTTP Client And Server Test Teardown Terminate HTTP Server *** Test Cases *** Receive And Reply To POST ${request body}= Set Variable { "method": "POST" } Send HTTP Request Async POST /post ${request body} Wait For Request Reply By 200 ${method}= Get Request Method ${url}= Get Request Url ${body}= Get Request Body ${body}= Decode Bytes To String ${body} UTF-8 Should Be Equal ${method} POST Should Be Equal ${url} /post Should Be Equal ${body} ${request body} *** Keywords *** Initialize HTTP Client And Server Initialize Client 127.0.0.1 8000 Start Server 127.0.0.1 8000 Terminate HTTP Server Stop Server In case of requirement to use IPv6 the keyword `Initialize HTTP Client And Server` might be the following: .. code:: robotframework *** Keywords *** Initialize HTTP Client And Server Initialize Client 0000:0000:0000:0000:0000:0000:0000:0001 8000 Start Server 0000:0000:0000:0000:0000:0000:0000:0001 8000 There is an example where server stubs are using. Sever stub is a pre-defined function that is used by the server to reply automatically to a request that satisfies a user specific HTTP criteria. .. code:: robotframework *** Settings *** Library String Library HttpCtrl.Client Library HttpCtrl.Server Test Setup Initialize HTTP Client And Server Test Teardown Terminate HTTP Server *** Test Cases *** Set Signle Stub And Send Request # Set server stub to reply automatically to POST /api/v1/post Set Stub Reply POST /api/v1/post 200 Post Message # Send HTTP request to the server Send HTTP Request POST /api/v1/post # Check that the client receives pre-defined values by the stub ${status}= Get Response Status ${body}= Get Response Body ${body}= Decode Bytes To String ${body} UTF-8 Should Be Equal ${status} ${200} Should Be Equal ${body} Post Message # Check that the server receives a single request ${count}= Get Stub Count POST /api/v1/post Should Be Equal ${count} ${1} *** Keywords *** Initialize HTTP Client And Server Initialize Client 127.0.0.1 8000 Start Server 127.0.0.1 8000 Terminate HTTP Server Stop Server """ def __init__(self): self.__response_headers = {} self.__request = None self.__server = None self.__thread = None def __del__(self): self.stop_server() def start_server(self, host, port): """ Start HTTP server on specific address and port. Server should be closed when it is not required, for example, when test is over. In case of double call of \`Start Server\`, the previous will be stopped and only then the next one HTTP server will be started. `host` [in] (string): Address that will be used by HTTP server to listen. `port` [in] (string): Port that will be used by HTTP server to listen. Example how to initialize server: +--------------+-----------+------+ | Start Server | 127.0.0.1 | 8000 | +--------------+-----------+------+ .. code:: text Start Server 127.0.0.1 8000 It is a good practice to start server and stop it using 'Test Setup' and 'Test Teardown', for example: .. code:: robotframework *** Settings *** Library HttpCtrl.Server Test Setup Initialize HTTP Server Test Teardown Terminate HTTP Server *** Test Cases *** HTTP Server Based Test Wait For Request Reply By 200 # Check incoming request *** Keywords *** Initialize HTTP Server Start Server 127.0.0.1 8000 Terminate HTTP Server Stop Server """ self.stop_server() logger.info("Prepare HTTP server '%s:%s' and thread to serve it." % (host, port)) self.__server = HttpServer(host, int(port)) self.__thread = threading.Thread(target=self.__server.start, args=()) self.__thread.start() # In case of start-stop, stop may be finished before server start and ir will be impossible to join thread. self.__server.wait_run_state() def stop_server(self): """ Stop HTTP server if it has been started. This function should be called if server has been started. Example how to stop server: +-------------+ | Stop Server | +-------------+ .. code:: text Stop Server It is a good practice to start server and stop it using `Test Setup` and `Test Teardown` - see example for \`Start Server\`. """ if self.__server is not None: self.__server.stop() self.__thread.join() self.__response_headers = {} self.__request = None ResponseStorage().clear() RequestStorage().clear() HttpStubContainer().clear() self.__server = None self.__thread = None logger.info("HTTP server is stopped.") def wait_for_request(self, timeout=5): """ Command to server to wait incoming request. This call is blocked until HTTP request arrives. Basically server receives all requests after \`Start Server\` and places them to internal queue. When test call function \`Wait For Request\` it checks the queue and if it is not empty returns the first request in the queue. If the queue is empty then function waits when the server receives request and place it to the queue. There is default time period '5 seconds' to wait request and this waiting time can be changed. If during wait time the request is not received then timeout error occurs. `timeout` [in] (int): Period of time in seconds when a request should be received by HTTP server. Example how to wait request. +------------------+ | Wait For Request | +------------------+ .. code:: text Wait For Request Example how to wait request during 2 seconds. +------------------+---+ | Wait For Request | 2 | +------------------+---+ .. code:: text Wait For Request 2 """ self.__request = RequestStorage().pop(int(timeout)) if self.__request is None: raise AssertionError("Timeout: request was not received.") logger.info("Request is received: %s" % self.__request) def wait_for_no_request(self, timeout=5.0): """ Command to server to wait for no incoming request during specific time. This call is blocked until HTTP request arrives or timeout. Basically server receives all requests after \`Start Server\` and places them to internal queue. When test call function \`Wait For No Request\` it checks the queue and if it is not empty returns throws exception. Otherwise it waits for request during 'timeout' seconds. If during this time request is received then exception is thrown. `timeout` [in] (int): Period of time in seconds when requests should not be received by HTTP server. Example how to wait for lack of requests. +---------------------+ | Wait For No Request | +---------------------+ .. code:: text Wait For No Request Example how to wait for lack of requests during 10 seconds. +---------------------+----+ | Wait For No Request | 10 | +---------------------+----+ .. code:: text Wait For No Request 10 """ self.__request = RequestStorage().pop(int(timeout)) if self.__request is not None: raise AssertionError("Request was received: %s." % self.__request) logger.info("Request is not received.") def wait_and_ignore_request(self): """ Command to server to wait incoming request and ignore it by closing connection. This call is blocked until HTTP request arrives. Basically server receives all requests after \`Start Server\` and places them to internal queue. When test call function \`Wait And Ignore Request\` it checks the queue and if it is not empty returns the first request in the queue is ignore and connection is closed. If the queue is empty then function waits when the server receives request and place it to the queue. Example how to wait and ignore request. +-------------------------+ | Wait And Ignore Request | +-------------------------+ .. code:: text Wait And Ignore Request """ self.wait_for_request() ResponseStorage().push(IgnoreRequest()) logger.info("Request is ignored by closing connection.") def set_stub_reply(self, method, url, status, body=None): """ Sets stub reply for HTTP(S) server. This function sets a server stub to reply automatically by a specific response to a specific request. When the stub is used to reply, then corresponding statistic is incremented (see \`Get Stub Count\`). `method` [in] (string): Request method that is used to handle by server stub (GET, POST, DELETE, etc., see: RFC 7231, RFC 5789). `url` [in] (string): Path to the resource that is used by server stub, for example, in case address www.httpbin.org/ip - '/ip' is an path. `status` [in] (int|string): HTTP status code for response that is used by server stub. `body` [in] (string|bytes): Response body that is used by server stub. Example how to set stub to reply automatically to request `POST` `/api/v1/request` by status `200`. +----------------+------+-----------------+-----+ | Set Stub Reply | POST | /api/v1/request | 200 | +----------------+------+-----------------+-----+ .. code:: text Set Stub Reply POST /api/v1/request 200 Example how to set stub to reply automatically using a specific body. +----------------+------+-----------------+-----+--------------------------+ | Set Stub Reply | POST | /api/v1/request | 202 | Request has been handled | +----------------+------+-----------------+-----+--------------------------+ .. code:: text Set Stub Reply POST /api/v1/request 200 Request has been handled """ if self.__server is None: message_error = "Impossible to set server stub reply (reason: 'server is not created')." raise AssertionError(message_error) criteria = HttpStubCriteria(method=method, url=url) response = Response(int(status), None, json.dumps(body), None, None) HttpStubContainer().add(criteria, response) def get_stub_count(self, method, url): """ Returns server stub statistic that defines how many time the stub was used by server to reply. `method` [in] (string): Request method that is used to handle by server stub (GET, POST, DELETE, etc., see: RFC 7231, RFC 5789). `url` [in] (string): Path to the resource that is used by server stub, for example, in case address www.httpbin.org/ip - '/ip' is an path. Example how to get server stub statistic for request with `POST` method and URL `/api/v2/request`. +----------------+------+-----------------+ | Get Stub Count | POST | /api/v2/request | +----------------+------+-----------------+ .. code:: text Get Stub Count POST /api/v2/request Example how to get server stub statistic for request with `GET` method and URL `/get` +----------------+------+-----+ | Get Stub Count | GET | /get | +----------------+------+-----+ .. code:: text Get Stub Count GET /get """ if self.__server is None: message_error = "Impossible to get server stub statistic (reason: 'server is not created')." raise AssertionError(message_error) criteria = HttpStubCriteria(method=method, url=url) return HttpStubContainer().count(criteria) def get_request_source_address(self): """ Returns source address (client address) of received request as string value. This function should be called after \`Wait For Request\`, otherwise None is returned. Example how to obtain source address of incoming request: +----------------------------+ | Get Request Source Address | +----------------------------+ .. code:: text ${source address}= Get Request Source Address """ return self.__request.get_source_address() def get_request_source_port(self): """ Returns source port (client port) of received request as string value. This function should be called after \`Wait For Request\`, otherwise None is returned. Example how to obtain source port of incoming request: +-------------------------+ | Get Request Source Port | +-------------------------+ .. code:: text ${source port}= Get Request Source Port """ return str(self.__request.get_source_port()) def get_request_source_port_as_integer(self): """ Returns source port (client port) of received request as integer value. This function should be called after \`Wait For Request\`, otherwise None is returned. Example how to obtain source port of incoming request: +------------------------------------+ | Get Request Source Port As Integer | +------------------------------------+ .. code:: text ${source port}= Get Request Source Port As Integer """ return self.__request.get_source_port() def get_request_method(self): """ Returns method of received request as a string. This function should be called after \`Wait For Request\`, otherwise None is returned. Example how to obtain method of incoming request: +--------------------+ | Get Request Method | +--------------------+ .. code:: text Get Request Method """ return self.__request.get_method() def get_request_body(self): """ Returns body of received request as a string. This function should be called after \`Wait For Request\`, otherwise None is returned. Example how to obtain body of incoming request: +------------------+ | Get Request Body | +------------------+ .. code:: text Get Request Body """ return self.__request.get_body() def get_request_headers(self): """ Returns headers of received request as a dictionary. This function should be called after \`Wait For Request\`, otherwise None is returned. Example how to obtain headers of incoming request: +---------------------+ | Get Request Headers | +---------------------+ .. code:: text Get Request Headers """ return self.__request.get_headers() def get_request_url(self): """ Returns URL of received request as a string. This function should be called after \`Wait For Request\`, otherwise None is returned. Example how to obtain URL of incoming request: +-----------------+ | Get Request Url | +-----------------+ .. code:: text Get Request Url """ return self.__request.get_url() def set_reply_header(self, key, value): """ Set or insert new (if it does not exist yet) header to HTTP response. To send response itself function \`Reply By\` is used. `key` [in] (string): HTTP header name. `value` [in] (string): HTTP header value. Example how to set header for HTTP response: +------------------+--------+----------------+ | Set Reply Header | Origin | 127.0.0.1:8000 | +------------------+--------+----------------+ .. code:: text Set Reply Header Origin 127.0.0.1:8000 Example how to set several headers for HTTP response: +------------------+-------------+----------------+ | Set Reply Header | Origin | 127.0.0.1:8001 | +------------------+-------------+----------------+ | Set Reply Header | City-Source | St.-Petersburg | +------------------+-------------+----------------+ .. code:: text Set Reply Header Origin 127.0.0.1:8000 Set Reply Header City-Source St.-Petersburg """ self.__response_headers[key] = value def reply_by(self, status, body=None): """ Send response using specified HTTP code and body. This function should be called after \`Wait For Request\`. `status` [in] (string): HTTP status code for response. `body` [in] (string|bytes): Body that should contain response. Example how to reply by 204 (No Content) to incoming request: +----------+-----+ | Reply By | 204 | +----------+-----+ .. code:: text Reply By 204 Example how to reply 200 (OK) with body to incoming request: +----------+-----+--------------------------+ | Reply By | 200 | { "status": "accepted" } | +----------+-----+--------------------------+ .. code:: text ${response body}= Set Variable { "status": "accepted" } Reply By 204 ${response body} Example how to reply with a body represented by a sequence of bytes: ..code:: text Wait For Request ${body bytes}= Evaluate bytes((0x0a, 0x12, 0x0a)) Reply By 200 ${body bytes} """ response = Response(int(status), None, body, None, self.__response_headers) ResponseStorage().push(response) class Json: """ Json library provide comprehensive interface to Robot Framework to work with JSON structures that are actively used for Internet communication nowadays. See other HttpCtrl libraries: - HttpCtrl.Client_ - HTTP/HTTP Client API for testing where easy-controlled HTTP/HTTPS client is required. - HttpCtrl.Server_ - HTTP Server API for testing where easy-controlled HTTP server is required. - HttpCtrl.Logging_ - Logging related API to configure the logging system that is used by HttpCtrl library. .. _HttpCtrl.Client: client.html .. _HttpCtrl.Server: server.html .. _HttpCtrl.Logging: logging.html Example where Json values are updated in a string and then read from it: .. code:: robotframework *** Settings *** Library HttpCtrl.Client Library HttpCtrl.Json *** Test Cases *** Write And Read Json Nesting Value ${json template}= Catenate ... { ... "book": { ... "title": "St Petersburg: A Cultural History", ... "author": "Solomon Volkov", ... "price": 0, ... "currency": "" ... } ... } ${catalog}= Set Json Value In String ${json template} book/price ${500} ${catalog}= Set Json Value In String ${catalog} book/currency RUB ${title}= Get Json Value From String ${catalog} book/title ${price}= Get Json Value From String ${catalog} book/price ${currency}= Get Json Value From String ${catalog} book/currency Here is an another example where Json array's values are updated and then read from it: .. code:: robotframework *** Settings *** Library HttpCtrl.Client Library HttpCtrl.Json *** Test Cases *** Write And Read Json Array Value ${json template}= Catenate ... { ... "array": [ ... "red", "green", "blue", "yellow" ... ] ... } ${colors}= Set Json Value In String ${json template} array/3 white ${red}= Get Json Value From String ${colors} array/0 ${green}= Get Json Value From String ${colors} array/1 ${blue}= Get Json Value From String ${colors} array/2 ${white}= Get Json Value From String ${colors} array/3 """ @staticmethod def get_json_value_from_string(json_string, path): """ Return value from Json that is represented by string. Be aware, returned value's type corresponds to its type in Json. `json_string` [in] (string): Json that is represented by string. `path` [in] (string): Path to Json node value. Example how to obtain Json value: +-----------+----------------------------+------------------------------------+------------+ | ${value}= | Get Json Value From String | { "book": { "title": "Unknown" } } | book/title | +-----------+----------------------------+------------------------------------+------------+ .. code:: text ${json}= Catenate ... { ... "book": { ... "title": "Unknown" ... } ... } ${value}= Get Json Value From String ${json} book/title """ json_content = json.loads(json_string) keys = path.split('/') current_element = json_content for key in keys: if isinstance(current_element, list): key = int(key) current_element = current_element[key] return current_element @staticmethod def set_json_value_in_string(json_string, path, value): """ Set value in Json that is represented by string. Be aware type of updated value should correspond to its type in Json, otherwise type will be changed with new value. `json_string` [in] (string): Json that is represented by string. `path` [in] (string): Path to Json node value. `value` [in] (any): New value for the Json node. Example how to set Json value: +-----------+--------------------------+------------------------------------+------------+--------+ | ${value}= | Set Json Value In String | { "book": { "title": "Unknown" } } | book/title | "Math" | +-----------+--------------------------+------------------------------------+------------+--------+ .. code:: text ${json}= Catenate ... { ... "book": { ... "title": "Unknown" ... } ... } ${value}= Set Json Value In String ${json} book/title "Math" Example how to set value in Json array node: +-----------+--------------------------+--------------------------------------+---------+---------+ | ${value}= | Set Json Value In String | { "array": [ "one", "two", "ten" ] } | array/2 | "three" | +-----------+--------------------------+--------------------------------------+---------+---------+ .. code:: text ${json}= Catenate ... { ... "array": [ ... "one", "two", "ten" ... ] ... } ${value}= Set Json Value In String ${json} array/2 "three" """ json_content = json.loads(json_string) keys = path.split('/') current_element = json_content for key in keys: if key == keys[-1]: if isinstance(current_element, list): key = int(key) current_element[key] = value else: if isinstance(current_element, list): key = int(key) current_element = current_element[key] return json.dumps(json_content) class Logging: """ Logging library provide functionality to configure the logging system that is used by HttpCtrl library. See other HttpCtrl libraries: - HttpCtrl.Client_ - HTTP/HTTP Client API for testing where easy-controlled HTTP/HTTPS client is required. - HttpCtrl.Server_ - HTTP Server API for testing where easy-controlled HTTP server is required. - HttpCtrl.Json_ - Json related API for testing where work with Json message is required. .. _HttpCtrl.Client: client.html .. _HttpCtrl.Server: server.html .. _HttpCtrl.Json: json.html """ @staticmethod def set_body_size_limit_to_log(body_size): """ Set body (HTTP request/response) size that is allowed to log. By default the library logs `512` symbols of the body. If the limit should be removed then `${None}` value can be provided to the function. The logging body limit protects test logs to be too large if tests use big data for testing. Example how to set the logging body limit to 1024 symbols: +----------------------------+------+ | Set Body Size Limit To Log | 1024 | +----------------------------+------+ .. code:: text Set Body Size Limit To Log 1024 Example how to remove the logging body limit: +----------------------------+---------+ | Set Body Size Limit To Log | ${None} | +----------------------------+---------+ .. code:: text Set Body Size Limit To Log ${None} """ LoggerAssistant.set_body_size(body_size)
libraries/robotframework-httpctrl/src/HttpCtrl/http_handler.py 0 → 100755 +125 −0 Viewed Changes for libraries/robotframework-httpctrl/src/HttpCtrl/http_handler.py: 125 added lines, 0 removed lines. Original line number Diff line number Diff line """ HttpCtrl library provides HTTP/HTTPS client and server API to Robot Framework to make REST API testing easy. Authors: Andrei Novikov Date: 2018-2022 Copyright: The 3-Clause BSD License """ from http.server import SimpleHTTPRequestHandler from robot.api import logger from HttpCtrl.internal_messages import TerminationRequest, IgnoreRequest from HttpCtrl.request import Request from HttpCtrl.request_storage import RequestStorage from HttpCtrl.response_storage import ResponseStorage from HttpCtrl.http_stub import HttpStubContainer, HttpStubCriteria class HttpHandler(SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): self.server_version = "HttpCtrl.Server/" self.sys_version = "" SimpleHTTPRequestHandler.__init__(self, *args, **kwargs) def do_GET(self): self.__default_handler('GET') def do_POST(self): self.__default_handler('POST') def do_PUT(self): self.__default_handler('PUT') def do_OPTIONS(self): self.__default_handler('OPTIONS') def do_HEAD(self): self.__default_handler('HEAD') def do_PATCH(self): self.__default_handler('PATCH') def do_DELETE(self): self.__default_handler('DELETE') def log_message(self, format, *args): return def log_error(self, format, *args): return def log_request(self, code='-', size='-'): return def __extract_body(self): body_length = int(self.headers.get('Content-Length', 0)) if body_length > 0: return self.rfile.read(body_length) return None def __default_handler(self, method): host, port = self.client_address[:2] body = self.__extract_body() logger.info("'%s' request is received from '%s:%s'." % (method, host, port)) stub = HttpStubContainer().get(HttpStubCriteria(method=method, url=self.path), body) if stub is not None: response = stub.response request = Request(host, port, method, self.path, self.headers, body) RequestStorage().push(request) else: request = Request(host, port, method, self.path, self.headers, body) RequestStorage().push(request) response = ResponseStorage().pop() if isinstance(response, TerminationRequest) or isinstance(response, IgnoreRequest): return try: self.__send_response(response) except Exception as exception: logger.info("Response was not sent to client due to reason: '%s'." % str(exception)) def __send_response(self, response): if response is None: logger.error("Response is not provided for incoming request.") return self.send_response(response.get_status()) headers = response.get_headers() if headers is not None: for key, value in headers.items(): self.send_header(key, value) body = response.get_body() if body is not None: if isinstance(body, str): body = body.encode("utf-8") self.send_header('Content-Length', str(len(body))) self.end_headers() if body is not None: self.wfile.write(body) No newline at end of file