diff --git a/.gitignore b/.gitignore index fb61ed9..3d45ddf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ # created by virtualenv automatically env/ .idea/ +Pipfile +.pytest_cache +.coverage \ No newline at end of file diff --git a/enums/client_messages_response.py b/enums/client_messages_response.py new file mode 100644 index 0000000..8713b69 --- /dev/null +++ b/enums/client_messages_response.py @@ -0,0 +1,5 @@ +import enum + + +class ClientMessagesResponse(enum.Enum): + incorrect_url_or_port = 'Incorrect URL-address or port' diff --git a/enums/requests_names.py b/enums/requests_names.py new file mode 100644 index 0000000..6d2b844 --- /dev/null +++ b/enums/requests_names.py @@ -0,0 +1,9 @@ +import enum + + +class RequestsNames(enum.Enum): + get = 'get' + post = 'post' + patch = 'patch' + delete = 'delete' + put = 'put' diff --git a/enums/server_validating_messages.py b/enums/server_validating_messages.py new file mode 100644 index 0000000..dea5260 --- /dev/null +++ b/enums/server_validating_messages.py @@ -0,0 +1,10 @@ +import enum + + +class ServerValidatingMessages(enum.Enum): + incorrect_url = 'You did not write URL-address' + incorrect_type_request = 'You did not write type of request' + incorrect_format_cookie = 'Incorrect format cookie: type of object which takes cookie must be dictionary' + incorrect_format_headers = 'Incorrect format headers: type of object which takes headers must be dictionary' + incorrect_body_type = 'Incorrect format of body: type of object which takes body is string' + is_validated = 'All parameters is valid' diff --git a/client/http_client.py b/http_client.py similarity index 63% rename from client/http_client.py rename to http_client.py index 38412ab..39e8d27 100644 --- a/client/http_client.py +++ b/http_client.py @@ -1,12 +1,14 @@ import socket +from enums.client_messages_response import ClientMessagesResponse + class HttpClient: def __init__(self, settings: dict): - self.__settings = settings + self._settings = settings - self.__HOST = self.__settings.get("url") - self.__PORT = int(self.__settings.get("port")) + self._HOST = self._settings.get("url") + self._PORT = self._settings.get("port") def get_data(self) -> str: """ @@ -17,13 +19,17 @@ def get_data(self) -> str: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: timeout = 10 - if self.__settings.get("timeout") is not None: - timeout = self.__settings.get("timeout") + if self._settings.get("timeout") is not None: + timeout = self._settings.get("timeout") s.settimeout(timeout) - s.connect((self.__HOST, self.__PORT)) - request = self.create_http_request(self.__settings).encode() + try: + s.connect((self._HOST, int(self._PORT))) + except (ValueError, socket.gaierror): + return str(ClientMessagesResponse.incorrect_url_or_port.value) + + request = self.create_http_request(self._settings).encode() try: s.sendall(request) @@ -33,22 +39,16 @@ def get_data(self) -> str: response = b"" try: - while True: - data = s.recv(4096) - - if not data: - break - - response += data + response = self.receive_data(s) except socket.timeout: pass except Exception as e: print('log: ' + str(e)) - close_request = self.create_http_close_request(self.__settings).encode() + close_request = self.create_http_close_request(self._settings) try: - s.sendall(close_request) + self.send_data(conn=s, data=close_request) except Exception as e: print('log: ' + str(e)) @@ -56,6 +56,41 @@ def get_data(self) -> str: return response + @staticmethod + def receive_data(conn: socket) -> bytes: + response = b"" + + while True: + data = conn.recv(4096) + + if not data: + break + + response += data + + return response + + @staticmethod + def send_data(conn: socket, data: str) -> int: + """ + Отправляет данные клиенту + :param conn: подключение, по которому нужно отправить эти данные + :param data: отправляемые данные + :return: количество отправленных байт + """ + data_to_send = data.encode('utf-8') + bytes_sent = 0 + + while bytes_sent < len(data_to_send): + sent = conn.send(data_to_send[bytes_sent:]) + + if sent == 0: + break + + bytes_sent += sent + + return bytes_sent + @staticmethod def get_headers(settings: dict) -> str: """ @@ -75,7 +110,7 @@ def get_headers(settings: dict) -> str: cookies.append(f'{key}={value};') if len(cookies) != 0: - cookies = 'Cookie: ' + ' '.join(cookies) + cookies = 'Cookie: ' + ' '.join(cookies) cookies = cookies[:-1] cookies += '\r\n' headers.append(cookies) diff --git a/http_server.py b/http_server.py new file mode 100644 index 0000000..0d77475 --- /dev/null +++ b/http_server.py @@ -0,0 +1,232 @@ +import re +import socket +import json + +from enums.client_messages_response import ClientMessagesResponse +from enums.requests_names import RequestsNames +from enums.server_validating_messages import ServerValidatingMessages +from http_client import HttpClient + + +class HttpServer: + """ + HTTP-сервер, который умеет принимать запросы от клиентов и, используя HttpClient возвращает нужные запросы + """ + + def __init__(self): + self.__HOST = '127.0.0.1' + self.__PORT = 8080 + + self._server = None + + self._working = None + + def start(self): + """ + Запускает сервер + """ + + try: + self._create_server() + except OSError as e: + self.stop() + print(e) + + if self._server is None: + return + + self._server.listen() + + self._start_server() + + while self._working: + if self._server is None: + break + + conn, addr = self._server.accept() + + print(f'Client connected by addr: {addr}') + + with conn: + data = conn.recv(8192).decode() + + origin = None + + if data.startswith('OPTIONS / HTTP/1.1'): + origin = re.search(r'(?<=Origin: )(.+?)(?=\r\n)', data).group(0) + + response = self.create_option_response(origin) + + conn.sendall(response.encode()) + + data = conn.recv(8190).decode() + + body = re.search(r'(?<=\r\n\r\n)(.+)', data).group(0) + settings = json.loads(body) + + settings_is_validated = HttpServer.validate_params(settings) + + if not settings_is_validated[0]: + response = HttpServer.create_bad_request_response( + origin, + settings_is_validated[1] + ) + + HttpServer.send_data(conn, response) + else: + client = HttpClient(settings) + client_data = client.get_data() + + if client_data == ClientMessagesResponse.incorrect_url_or_port.value: + response = HttpServer.create_bad_request_response(origin, client_data) + HttpServer.send_data(conn, response) + else: + data = client_data + + if ( + settings.get('request').lower() == RequestsNames.get.value + and + len(settings.get('get_form')) != 0 + ): + data = self._take_data(settings, client_data) + + response = HttpServer.create_ok_request_response(data, origin) + HttpServer.send_data(conn, response) + + print(f'Client with addr {addr} was disconnected') + + def _stop_server(self): + self._working = False + + def _start_server(self): + self._working = True + + def stop(self): + self._stop_server() + + @staticmethod + def _take_data(settings: dict, client_data: str): + data = client_data + + if 'application/json' in client_data: + matches = re.finditer(settings.get('get_form'), client_data, re.IGNORECASE) + data = [match.start() for match in matches] + + return data + + def _create_server(self): + self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._server.bind((self.__HOST, self.__PORT)) + + def stop(self): + self._server.close() + self._server = None + + @staticmethod + def send_data(conn: socket, response: str) -> int: + """ + Отправляет данные на сервер + :param conn: подключение, по которому нужно отправить эти данные + :param response: отправляемые данные + :return: количество отправленных байт + """ + data_to_send = response.encode('utf-8') + bytes_sent = 0 + + while bytes_sent < len(data_to_send): + sent = conn.send(data_to_send[bytes_sent:]) + + bytes_sent += sent + + return bytes_sent + + @staticmethod + def validate_params(body: dict) -> tuple: + """ + Проверяет являются ли параметры запроса валидными + :return: возвращает tuple, где первый параметр - это true или false: результат валидности, + а второй сообщение описывающее результат валидации + """ + + if body.get('url') is None: + return False, ServerValidatingMessages.incorrect_url.value + + if body.get('request') is None: + return False, ServerValidatingMessages.incorrect_type_request.value + + if not isinstance(body.get('cookie'), dict): + return False, ServerValidatingMessages.incorrect_format_cookie.value + + if not isinstance(body.get('headers'), dict): + return False, ServerValidatingMessages.incorrect_format_headers.value + + if not isinstance(body.get('body'), str): + return False, ServerValidatingMessages.incorrect_body_type.value + + return True, ServerValidatingMessages.is_validated.value + + @staticmethod + def create_option_response(origin: str): + """ + Создает ответ на OPTION запрос клиента + :param origin: значение заголовка Origin в Option запросе клиента + :return: возвращает готовый ответ для клиента в формате протокола HTTP + """ + + return f"HTTP/1.1 200 OK\r\n" \ + f"Access-Control-Allow-Origin: {origin}\r\n" \ + f"Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n" \ + f"Access-Control-Allow-Headers: Content-Type\r\n" \ + f"Connection: keep-alive\r\n" \ + f"Content-Length: 0\r\n" \ + f"\r\n" + + @staticmethod + def create_response(code: int, code_message: str, data: str, origin: str,): + """ + Создает ответ на пользовательский запрос + :param code: код ответа + :param code_message: код ответа + :param data: данные в теле ответа (должны быть в json формате) + :param origin: значение заголовка Origin в Option запросе клиента + :return: ответ + """ + + response = f'HTTP/1.1 {code} {code_message}\r\n' \ + f'Content-Type: application/json\r\n' \ + f'Content-Length: {len(data.encode())}\r\n' \ + f'Connection: keep-alive\r\n' \ + f'Access-Control-Allow-Origin: {origin}' \ + f'\r\n\r\n{data}' + + return response + + @staticmethod + def create_ok_request_response(data: str, origin: str): + """ + Создает ответ на запрос с кодом успеха + :param data: данные, которые будут помещены в тело запроса + :param origin: значение заголовка Origin в Option запросе клиента + :return: возвращает готовый ответ для клиента в формате протокола HTTP + """ + + body = dict() + + body['data'] = data + body['code'] = 200 + + return HttpServer.create_response(body['code'], 'OK', json.dumps(body), origin) + + @staticmethod + def create_bad_request_response(origin: str, message: str): + """ + Создает ответ с кодом плохого запроса (bad request) + :return: + """ + + body = dict() + + body['message'] = message + body['code'] = 400 + + return HttpServer.create_response(body['code'], 'BAD REQUEST', json.dumps(body), origin) diff --git a/main.py b/main.py index c6f2d73..7ad8a3e 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,16 @@ +from http_server import HttpServer -from server.server import HttpServer -if __name__ == '__main__': - server = HttpServer() - server.start() +class Main: + def __init__(self): + self._server = HttpServer() + + def run(self): + self._server.start() + + def finish(self): + self._server.stop() + +if __name__ == '__main__': + Main().run() diff --git a/resources/index.html b/resources/index.html index cb57976..f10db42 100644 --- a/resources/index.html +++ b/resources/index.html @@ -20,25 +20,26 @@ -
-

HTTP-Client

-

Ошибка! Вы не ввели либо url, либо port, либо тип запроса!

-
-
-
- - -
-
- - -
-
- - -
-
-
Тип запроса:
+
+

HTTP-Client

+

Ошибка! Вы не ввели либо url, либо port, либо тип запроса!

+
+ +
+ + +
+
+ + +
+
+ + +
+
+
Тип запроса:
+
@@ -67,59 +68,68 @@

HTTP-Client

-
-
-
- -

- * - Заголовки должны быть записаны в формате:
- Заголовок: значение; - И обязательно должны быть разделены точкой с запятой -

-
- +
+
+ +

+ * + Заголовки должны быть записаны в формате:
+ Заголовок: значение; + И обязательно должны быть разделены точкой с запятой +

-
- - + +
+ -
-
-

Результат запроса:

-

+
+
+ + +
+ + +
+
+
+

Результат запроса:

+

+
diff --git a/resources/main.css b/resources/main.css index 3d958af..adea90c 100644 --- a/resources/main.css +++ b/resources/main.css @@ -115,6 +115,24 @@ textarea::placeholder { width: 35%; } +.request_btns { + display: flex; + align-items: flex-start; + justify-content: space-between; + + width: 100%; +} + +.get_form { + display: none; + + margin-left: 15px; +} + +.get_form__active { + display: block; +} + /* Wrappers styles */ .form_wrapper { @@ -255,13 +273,7 @@ textarea::placeholder { /* Result styles */ .result-container { - flex-direction: column; -} - -.result__text { - font-size: 15px; - color: #333; - font-weight: 400; + flex-direction: column; } .result { @@ -281,8 +293,13 @@ textarea::placeholder { } .result__text { - width: 100%; + font-size: 15px; + color: #333; + font-weight: 400; + + width: 100%; - hyphens: auto; - white-space: pre-wrap; + hyphens: auto; + white-space: pre-wrap; + word-wrap: break-word; } \ No newline at end of file diff --git a/resources/main.js b/resources/main.js index e0bbda5..8c157c9 100644 --- a/resources/main.js +++ b/resources/main.js @@ -49,6 +49,10 @@ const onSubmit = (event) => { const body = JSON.stringify(fields) + if (fields.request !== 'get') { + fields.get_form = '' + } + fetch('http://127.0.0.1:8080/', { method: 'post', headers: { @@ -58,16 +62,19 @@ const onSubmit = (event) => { }) .then(response => response.text()) .then(data => { - const result_text = document.querySelector('.result__text'); - result_text.textContent = data; + const result_text = document.querySelector('.result__text'); + + const jsonResponse = JSON.parse(data) - const result = document.querySelector('.result'); + result_text.textContent = JSON.stringify(jsonResponse) - if (!result.classList.contains('result-active')) { - result.classList.add('result-active') + const result = document.querySelector('.result'); + + if (!result.classList.contains('result-active')) { + result.classList.add('result-active') + } } - } - ); + ); return true } @@ -117,6 +124,18 @@ const parse = (headers) => { return response.length === 0 ? {} : response } -window.addEventListener('load', () => { +const radioBtnChecker = e => { + if (e.target.tagName.toLowerCase() === 'input' && e.target.id.toLowerCase() === 'get-request') { + document.querySelector('#get_form').classList.add('get_form__active'); + return null + } + + document.querySelector('#get_form').classList.remove('get_form__active'); +} + +const main = () => { document.querySelector('.form').addEventListener('submit', onSubmit) -}) \ No newline at end of file + document.querySelector('.request-btn_container').addEventListener('click', radioBtnChecker) +} + +window.addEventListener('load', main) \ No newline at end of file diff --git a/server/server.py b/server/server.py deleted file mode 100644 index 26fc6bb..0000000 --- a/server/server.py +++ /dev/null @@ -1,93 +0,0 @@ -import re -import socket -import json - -from client.http_client import HttpClient - - -class HttpServer: - """ - HTTP-сервер, который умеет принимать запросы от клиентов и, используя HttpClient возвращает нужные запросы - """ - - def __init__(self): - self.__HOST = '127.0.0.1' - self.__PORT = 8080 - - def start(self): - """ - Запускает сервер - """ - - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind((self.__HOST, self.__PORT)) - s.listen() - - while True: - conn, addr = s.accept() - - print(f'Client connected by addr: {addr}') - - with conn: - data = conn.recv(8190).decode() - origin = None - - if data.startswith('OPTIONS / HTTP/1.1'): - origin = re.search(r'(?<=Origin: )(.+?)(?=\r\n)', data).group(0) - - response = self.create_option_response(origin) - - conn.sendall(response.encode()) - - data = conn.recv(8190).decode() - - body = re.search(r'(?<=\r\n\r\n)(.+)', data).group(0) - settings = json.loads(body) - - client = HttpClient(settings) - - client_data = client.get_data() - - response = self.create_response(client_data, origin) - conn.sendall(response.encode()) - - print(f'Client with addr {addr} was disconnected') - - @staticmethod - def create_option_response(origin: str): - """ - Создает ответ на OPTION запрос клиента - :param origin: значение заголовка Origin в Option запросе клиента - :return: возвращает готовый ответ для клиента в формате протокола HTTP - """ - - return f"HTTP/1.1 200 OK\r\n" \ - f"Access-Control-Allow-Origin: {origin}\r\n" \ - f"Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n" \ - f"Access-Control-Allow-Headers: Content-Type\r\n" \ - f"Connection: keep-alive\r\n" \ - f"Content-Length: 0\r\n" \ - f"\r\n" - - @staticmethod - def create_response(data: str, origin: str): - """ - Создает ответ на запрос - :param data: данные, которые будут помещены в тело запроса - :param origin: значение заголовка Origin в Option запросе клиента - :return: возвращает готовый ответ для клиента в формате протокола HTTP - """ - - response = f'HTTP/1.1 200 OK\r\n' \ - f'Content-Type: application/json\r\n' \ - f'Content-Length: {len(data.encode())}\r\n' \ - f'Connection: keep-alive\r\n' \ - f'Access-Control-Allow-Origin: {origin}' \ - f'\r\n\r\n' - - if len(data) != 0: - response += json.dumps({ - 'data': data - }) - - return response diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/main_tests.py b/tests/main_tests.py deleted file mode 100644 index 3a14dca..0000000 --- a/tests/main_tests.py +++ /dev/null @@ -1,57 +0,0 @@ -import unittest - -from client.http_client import HttpClient -from test_server import TestServer - - -# ToDo: сделать тесты ещё для методов класса HttpClient: create_http_request, create_http_close_request и get_headers -# ToDo: а также сделать тесты для класса HttpServer: create_option_response и create_response - -class Tests(unittest.TestCase): - def test_client(self): - settings = { - 'url': '127.0.0.1', - 'port': 4444, - 'cookie': { - 'name': 'ilya', - 'surname': 'fomko' - }, - 'headers': { - 'Host': '127.0.0.1' - }, - 'body': 'Привет, мир!', - 'request': 'GET' - } - - client = HttpClient(settings) - TestServer().test() - - result = client.get_data() - - self.assertEqual(HttpClient.create_http_request(settings), result) - - def test_create_request(self): - settings = { - 'url': '127.0.0.1', - 'port': 4444, - 'cookie': { - 'name': 'ilya', - 'surname': 'fomko' - }, - 'headers': { - 'Host': '127.0.0.1' - }, - 'body': 'Привет, мир!', - 'request': 'GET' - } - - expected_result = f'GET / HTTP/1.1\r\n' + \ - f'Host: 127.0.0.1\r\n' \ - f'Cookie: name=ilya; surname=fomko\r\n\r\n' \ - f'Привет, мир!' - - self.assertEqual(HttpClient.create_http_request(settings), expected_result) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_http_client.py b/tests/test_http_client.py new file mode 100644 index 0000000..cc369fb --- /dev/null +++ b/tests/test_http_client.py @@ -0,0 +1,189 @@ +import unittest +from unittest.mock import MagicMock + +from enums.client_messages_response import ClientMessagesResponse +from http_client import HttpClient + + +class TestClient(unittest.TestCase): + def test_create_request(self): + settings = { + 'url': '127.0.0.1', + 'port': 4444, + 'cookie': { + 'name': 'ilya', + 'surname': 'fomko' + }, + 'headers': { + 'Host': '127.0.0.1' + }, + 'body': 'Привет, мир!', + 'request': 'GET' + } + + expected_result = f'GET / HTTP/1.1\r\n' + \ + f'Host: 127.0.0.1\r\n' \ + f'Cookie: name=ilya; surname=fomko\r\n\r\n' \ + f'Привет, мир!' + + self.assertEqual(HttpClient.create_http_request(settings), expected_result) + + # + def test_create_request_without_cookie(self): + settings = { + 'url': '127.0.0.1', + 'port': 4444, + 'cookie': {}, + 'headers': { + 'Host': '127.0.0.1' + }, + 'body': 'Привет, мир!', + 'request': 'GET' + } + + expected_result = f'GET / HTTP/1.1\r\n' + \ + f'Host: 127.0.0.1\r\n\r\n' \ + f'Привет, мир!' + + self.assertEqual(HttpClient.create_http_request(settings), expected_result) + + def test_create_request_without_headers(self): + settings = { + 'url': '127.0.0.1', + 'port': 4444, + 'cookie': { + 'name': 'ilya', + 'surname': 'fomko' + }, + 'headers': {}, + 'body': 'Привет, мир!', + 'request': 'GET' + } + + expected_result = f'GET / HTTP/1.1\r\n' + \ + f'Cookie: name=ilya; surname=fomko\r\n\r\n' \ + f'Привет, мир!' + + self.assertEqual(HttpClient.create_http_request(settings), expected_result) + + def test_create_headers_string_with_cookie(self): + settings = { + 'url': '127.0.0.1', + 'port': 4444, + 'cookie': { + 'name': 'ilya', + 'surname': 'fomko' + }, + 'headers': { + 'Host': '127.0.0.1', + 'Connection': 'keep-alive' + }, + 'body': 'Привет, мир!', + 'request': 'GET' + } + + expected_result = 'Host: 127.0.0.1\r\nConnection: keep-alive\r\nCookie: name=ilya; surname=fomko\r\n' + + self.assertEqual(HttpClient.get_headers(settings), expected_result) + + def test_create_headers_string_without_cookie(self): + settings = { + 'url': '127.0.0.1', + 'port': 4444, + 'cookie': {}, + 'headers': { + 'Host': '127.0.0.1', + 'Connection': 'keep-alive' + }, + 'body': 'Привет, мир!', + 'request': 'GET' + } + + expected_result = 'Host: 127.0.0.1\r\nConnection: keep-alive\r\n' + + self.assertEqual(HttpClient.get_headers(settings), expected_result) + + def test_create_close_http_request(self): + settings = { + 'url': '127.0.0.1', + 'request': 'GET' + } + + expected_result = 'GET / HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n' + + self.assertEqual(HttpClient.create_http_close_request(settings), expected_result) + + def test_create_close_request(self): + settings = { + 'url': '127.0.0.1', + 'port': 4444, + 'cookie': { + 'name': 'ilya', + 'surname': 'fomko' + }, + 'headers': { + 'Host': '127.0.0.1' + }, + 'body': 'Привет, мир!', + 'request': 'GET' + } + + expected_result = 'GET / HTTP/1.1\r\n' \ + 'Host: 127.0.0.1\r\n' \ + 'Connection: close\r\n\r\n' + + self.assertEqual(expected_result, HttpClient.create_http_close_request(settings)) + + def test_create_headers(self): + settings = { + 'url': '127.0.0.1', + 'port': 4444, + 'cookie': { + 'name': 'ilya', + 'surname': 'fomko' + }, + 'headers': { + 'Host': '127.0.0.1' + }, + 'body': 'Привет, мир!', + 'request': 'GET' + } + + expected_result = 'Host: 127.0.0.1\r\nCookie: name=ilya; surname=fomko\r\n' + + self.assertEqual(expected_result, HttpClient.get_headers(settings)) + + def test_receive_data(self): + socket = MagicMock() + socket.recv.side_effect = [b'test_data', None] + + expected_result = b'test_data' + + self.assertEqual(expected_result, HttpClient.receive_data(socket)) + + def test_send_data(self): + data = 'test send data' + + expected_result = len(data.encode()) + + socket = MagicMock() + socket.send.side_effect = [expected_result, 0] + + self.assertEqual(expected_result, HttpClient.send_data(socket, data)) + + def test_connect_with_host_or_port_exc(self): + settings = { + 'url': 'xcvx', + 'port': -234, + 'cookie': { + 'name': 'ilya', + 'surname': 'fomko' + }, + 'headers': { + 'Host': '127.0.0.1' + }, + 'body': 'Привет, мир!', + 'request': 'GET' + } + + self.assertEqual(str(ClientMessagesResponse.incorrect_url_or_port.value), HttpClient(settings).get_data()) diff --git a/tests/test_http_server.py b/tests/test_http_server.py new file mode 100644 index 0000000..ec6280a --- /dev/null +++ b/tests/test_http_server.py @@ -0,0 +1,228 @@ +import json +import re +import socket +import unittest +from unittest.mock import MagicMock + +from enums.server_validating_messages import ServerValidatingMessages +from http_server import HttpServer + + +class TestServer(unittest.TestCase): + def test_validate_params_positive(self): + settings = { + 'url': '127.0.0.1', + 'port': 4444, + 'cookie': { + 'name': 'ilya', + 'surname': 'fomko' + }, + 'headers': {}, + 'body': 'Привет, мир!', + 'request': 'GET' + } + + self.assertEqual(HttpServer.validate_params(settings)[0], True) + + def test_validate_params_negative_1(self): + settings = { + 'port': 4444, + 'cookie': { + 'name': 'ilya', + 'surname': 'fomko' + }, + 'body': 'Привет, мир!', + 'request': 'GET' + } + + self.assertEqual(HttpServer.validate_params(settings)[0], False) + + def test_validate_params_negative_2(self): + settings = { + 'url': '127.0.0.1', + 'port': 4444, + 'cookie': { + 'name': 'ilya', + 'surname': 'fomko' + }, + 'body': 'Привет, мир!', + } + + self.assertEqual(HttpServer.validate_params(settings)[0], False) + + def test_validate_params_negative_3(self): + settings = { + 'url': '127.0.0.1', + 'port': 4444, + 'cookie': { + 'name': 'ilya', + 'surname': 'fomko' + }, + 'body': 'Привет, мир!', + 'request': 'GET' + } + + self.assertEqual(HttpServer.validate_params(settings)[0], False) + + def test_validate_params_negative_4(self): + settings = { + 'url': '127.0.0.1', + 'port': 4444, + 'headers': {}, + 'body': 'Привет, мир!', + 'request': 'GET' + } + + self.assertEqual(HttpServer.validate_params(settings)[0], False) + + def test_validate_params_negative_5(self): + settings = { + 'url': '127.0.0.1', + 'port': 4444, + 'headers': {}, + 'body': 234, + 'request': 'GET' + } + + self.assertEqual(HttpServer.validate_params(settings)[0], False) + + def test_validate_params_negative_6(self): + settings = { + 'url': '127.0.0.1', + 'port': 4444, + 'headers': 23423, + 'body': '', + 'request': 'GET' + } + + self.assertEqual(HttpServer.validate_params(settings)[0], False) + + def test_validate_params_negative_7(self): + settings = { + 'url': '127.0.0.1', + 'port': 4444, + 'cookie': 1234, + 'headers': {}, + 'body': None, + 'request': 'GET' + } + + self.assertEqual(HttpServer.validate_params(settings)[0], False) + + def test_create_option_response(self): + origin = '127.0.0.1' + + expected_result = "HTTP/1.1 200 OK\r\n" \ + "Access-Control-Allow-Origin: 127.0.0.1\r\n" \ + "Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n" \ + "Access-Control-Allow-Headers: Content-Type\r\n" \ + "Connection: keep-alive\r\n" \ + "Content-Length: 0\r\n" \ + "\r\n" + + self.assertEqual(HttpServer.create_option_response(origin), expected_result) + + def test_create_response_with_body(self): + code = 200 + code_message = 'OK' + body = json.dumps({ + 'message': 'все супер! ты молодец' + }) + origin = '127.0.0.1' + + expected_result = 'HTTP/1.1 200 OK\r\n' \ + 'Content-Type: application/json\r\n' \ + f'Content-Length: {len(body.encode())}\r\n' \ + 'Connection: keep-alive\r\n' \ + 'Access-Control-Allow-Origin: 127.0.0.1' \ + '\r\n\r\n' + \ + f'{body}' + + self.assertEqual(HttpServer.create_response(code, code_message, body, origin), expected_result) + + def test_create_response_without_body(self): + code = 200 + code_message = 'OK' + body = '' + origin = '127.0.0.1' + + expected_result = 'HTTP/1.1 200 OK\r\n' \ + 'Content-Type: application/json\r\n' \ + f'Content-Length: {len(body.encode())}\r\n' \ + 'Connection: keep-alive\r\n' \ + 'Access-Control-Allow-Origin: 127.0.0.1' \ + '\r\n\r\n' + + self.assertEqual(HttpServer.create_response(code, code_message, body, origin), expected_result) + + def test_send_data(self): + data = 'test send data' + + expected_result = len(data.encode()) + + socket = MagicMock() + socket.send.side_effect = [expected_result] + + self.assertEqual(expected_result, HttpServer.send_data(socket, data)) + + def test_create_ok_response(self): + data = 'Hello, World!' + origin = '127.0.0.1' + + body = dict() + body['data'] = data + body['code'] = 200 + + body_json = json.dumps(body) + + expected_result = f'HTTP/1.1 200 OK\r\n' \ + f'Content-Type: application/json\r\n' \ + f'Content-Length: {len(body_json.encode())}\r\n' \ + f'Connection: keep-alive\r\n' \ + f'Access-Control-Allow-Origin: {origin}' \ + f'\r\n\r\n{body_json}' + + self.assertEqual(expected_result, HttpServer.create_ok_request_response(data, origin)) + + def test_create_bad_response(self): + message = 'Hello, World!' + origin = '127.0.0.1' + + body = dict() + body['message'] = message + body['code'] = 400 + + body_json = json.dumps(body) + + expected_result = f'HTTP/1.1 400 BAD REQUEST\r\n' \ + f'Content-Type: application/json\r\n' \ + f'Content-Length: {len(body_json.encode())}\r\n' \ + f'Connection: keep-alive\r\n' \ + f'Access-Control-Allow-Origin: {origin}' \ + f'\r\n\r\n{body_json}' + + self.assertEqual(expected_result, HttpServer.create_bad_request_response(origin, message)) + + def test_take_data(self): + client_data = 'Content-Type: application/json\r\n{"data": "test test test"}' + settings = {'get_form': 'test'} + + matches = re.finditer(settings.get('get_form'), client_data, re.IGNORECASE) + expected_result = [match.start() for match in matches] + + self.assertEqual(expected_result, HttpServer._take_data(settings, client_data)) + + def test_start_working(self): + server = HttpServer() + + server._start_server() + + self.assertEqual(True, server._working) + + def test_stop_working(self): + server = HttpServer() + + server._start_server() + server._stop_server() + + self.assertEqual(False, server._working) diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..b561b60 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,24 @@ +import unittest +from threading import Thread + +from main import Main + + +def run_and_close_main(main: 'Main'): + main.run() + + +class TestMain(unittest.TestCase): + def test_main_func(self): + main = Main() + main_thread = Thread( + target=run_and_close_main, + args=[main] + ) + + main_thread.daemon = True + main_thread.start() + + main.finish() + + assert True diff --git a/tests/test_server.py b/tests/test_server.py deleted file mode 100644 index e22e07c..0000000 --- a/tests/test_server.py +++ /dev/null @@ -1,34 +0,0 @@ -import socket - -from threading import Thread - - -class TestServer: - def __init__(self): - super().__init__() - self.__HOST = '127.0.0.1' - self.__PORT = 4444 - - def __run_server(self): - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.settimeout(15) - s.bind((self.__HOST, self.__PORT)) - - s.listen() - - conn, addr = s.accept() - - with conn: - try: - while True: - data = conn.recv(4096) - - if not data: - break - - conn.sendall(data) - except Exception as e: - print('log: ' + str(e)) - - def test(self): - Thread(target=self.__run_server).start()