diff --git a/.gitignore b/.gitignore index 328108bf..d3d7ee82 100644 --- a/.gitignore +++ b/.gitignore @@ -137,7 +137,7 @@ dmypy.json .idea ## Morelia stuff -/database/database.db db_sqlite.db - +config.ini t.py +ngrok.exe diff --git a/Pipfile b/Pipfile index fe50c90c..bdc65f12 100644 --- a/Pipfile +++ b/Pipfile @@ -5,17 +5,21 @@ verify_ssl = true [dev-packages] flake8 = "*" +pyflakes = "*" +pycodestyle = "*" +mccabe = "*" [packages] -uvicorn = "==0.12.1" -fastapi = "==0.65.2" -jinja2 = "==2.11.3" -aiofiles = "==0.5.0" -email-validator = "==1.1.1" -sqlobject = "==3.8.1" +uvicorn = "==0.14.0" +fastapi = "==0.66.0" +jinja2 = "==3.0.1" +aiofiles = "==0.7.0" +email-validator = "==1.1.3" +sqlobject = "==3.9.1" loguru = "==0.5.3" websockets = "==9.1" -pydantic = "==1.6.2" +pydantic = "==1.8.2" +websocket-client = "*" [requires] -python_version = "3.8" +python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock index aba90a07..b403f924 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "a8f5f6f51dcb14d84bd4184c957906918658e9afa06cc1655d596a2c165604bf" + "sha256": "1b612b47bc072a5fc0c9fe264fd4a19a9fcdb525805aba17e52cf379ad46b32e" }, "pipfile-spec": 6, "requires": { - "python_version": "3.8" + "python_version": "3.9" }, "sources": [ { @@ -18,26 +18,34 @@ "default": { "aiofiles": { "hashes": [ - "sha256:377fdf7815cc611870c59cbd07b68b180841d2a2b79812d8c218be02448c2acb", - "sha256:98e6bcfd1b50f97db4980e182ddd509b7cc35909e903a8fe50d8849e02d815af" + "sha256:a1c4fc9b2ff81568c83e21392a82f344ea9d23da906e4f6a52662764545e19d4", + "sha256:c67a6823b5f23fcab0a2595a289cec7d8c863ffcb4322fb8cd6b90400aedfdbc" ], "index": "pypi", - "version": "==0.5.0" + "version": "==0.7.0" + }, + "asgiref": { + "hashes": [ + "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9", + "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214" + ], + "markers": "python_version >= '3.6'", + "version": "==3.4.1" }, "click": { "hashes": [ - "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", - "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", + "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==7.1.2" + "markers": "python_version >= '3.6'", + "version": "==8.0.1" }, "colorama": { "hashes": [ "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], - "markers": "sys_platform == 'win32'", + "markers": "sys_platform == 'win32' and platform_system == 'Windows'", "version": "==0.4.4" }, "dnspython": { @@ -50,19 +58,19 @@ }, "email-validator": { "hashes": [ - "sha256:5f246ae8d81ce3000eade06595b7bb55a4cf350d559e890182a1466a21f25067", - "sha256:63094045c3e802c3d3d575b18b004a531c36243ca8d1cec785ff6bfcb04185bb" + "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b", + "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7" ], "index": "pypi", - "version": "==1.1.1" + "version": "==1.1.3" }, "fastapi": { "hashes": [ - "sha256:39569a18914075b2f1aaa03bcb9dc96a38e0e5dabaf3972e088c9077dfffa379", - "sha256:8359e55d8412a5571c0736013d90af235d6949ec4ce978e9b63500c8f4b6f714" + "sha256:6ea4225448786f3d6fae737713789f87631a7455f65580de0a4a2e50471060d9", + "sha256:85d8aee8c3c46171f4cb7bb3651425a42c07cb9183345d100ef55d88ca2ce15f" ], "index": "pypi", - "version": "==0.65.2" + "version": "==0.66.0" }, "formencode": { "hashes": [ @@ -80,7 +88,7 @@ "markers": "python_version >= '3.6'", "version": "==0.12.0" }, - "jinja2": { + "idna": { "hashes": [ "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" @@ -88,13 +96,13 @@ "markers": "python_version >= '3.5'", "version": "==3.2" }, - "loguru": { + "jinja2": { "hashes": [ - "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", - "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" + "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", + "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" ], "index": "pypi", - "version": "==2.11.3" + "version": "==3.0.1" }, "loguru": { "hashes": [ @@ -146,26 +154,31 @@ }, "pydantic": { "hashes": [ - "sha256:14e598055b65d2e6cedf10dc3de6ad1bb04ca3eec348e4af1cf5e5e496deab55", - "sha256:1d42c7408cde8a224c2bb969bfb9dca21f3b1eec37a92044be8bcd7d35ea5826", - "sha256:433dda6200104d7aa38c27a6ea52485e69931042556065402281cc73a57fd680", - "sha256:51dec047b44f0de4dbfa301b73df605918088348b951b8b4616127249febfe30", - "sha256:548c284237b0c61e0e785ad03167c75723f22000f82e8104d8981fdf50ce14e8", - "sha256:60b8956b57045224294691b78a6a4be0f321271a9f1c2a7fef25248e4c4f20df", - "sha256:6eea211e8b427841a16f43fa739ac06059db6af0d167476b928dbb237d870b77", - "sha256:76b241172d6e22403e116e1d3b7305b6a9479323f8168f2fcb300ffe698443b9", - "sha256:90310c1c5945b4fe2ff7bd69e306e54d192e55d22480657ddd6d2519cf2f12ba", - "sha256:93f7f510fc366b99dace4a3d1f036aafcfe908092c5f2572ad4a96be24da199c", - "sha256:ae48129396bd5acfaef1cdaaa959ac8ab5d02c026b1fdffb421dc6fa81d7861d", - "sha256:be4e0263ef515ae14f06e9fb372843f00bdb218ec4f2f04beb3480ac1538a9a9", - "sha256:bf9e5dd5e0e7e64541508f657c63bf6ab869109cb39f017f935acfeb64ea9be8", - "sha256:cd777c102ba31bc9992093c2e9f778c21b3965566d1fa5ac9f9b7cea2e67fe2a", - "sha256:d09adff1c70351a8750941dd39fda25447eab2e3cdb5b2aade340f69f6f53e84", - "sha256:e77e5f640f1093bf417b841d9b4148bd4212bb0dbb2cbb9024aa07f2b3b260eb", - "sha256:f6a1a465dd72aff0462486588a2bf905f9169e575deec1e6f6d00240fe1b4e00" + "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd", + "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739", + "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f", + "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840", + "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23", + "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287", + "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62", + "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b", + "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb", + "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820", + "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3", + "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b", + "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e", + "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3", + "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316", + "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b", + "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4", + "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20", + "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e", + "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505", + "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1", + "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833" ], "index": "pypi", - "version": "==1.6.2" + "version": "==1.8.2" }, "pydispatcher": { "hashes": [ @@ -184,13 +197,13 @@ }, "sqlobject": { "hashes": [ - "sha256:08080da9ce7f51df8b923002884b28613249addb39efddd0cf263295c1253ad9", - "sha256:620657105ab5720658222d10ad13c52281fe524137b59ab166eee4427ee2f548" + "sha256:45064184decf7f42d386704e5f47a70dee517d3e449b610506e174025f84d921", + "sha256:ec2b09215d181506d247096bc2c4abebc8e33a25212b25c676f3e3862d1150ea" ], "index": "pypi", - "version": "==3.8.1" + "version": "==3.9.1" }, - "six": { + "starlette": { "hashes": [ "sha256:3c8e48e52736b3161e34c9f0e8153b4f32ec5d8995a3ee1d59410d92f75162ed", "sha256:7d49f4a27f8742262ef1470608c59ddbc66baf37c148e938c7038e6bc7a998aa" @@ -200,19 +213,27 @@ }, "typing-extensions": { "hashes": [ - "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", - "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", - "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" ], - "version": "==3.7.4.3" + "version": "==3.10.0.0" }, "uvicorn": { "hashes": [ - "sha256:a461e76406088f448f36323f5ac774d50e5a552b6ccb54e4fca8d83ef614a7c2", - "sha256:d06a25caa8dc680ad92eb3ec67363f5281c092059613a1cc0100acba37fc0f45" + "sha256:2a76bb359171a504b3d1c853409af3adbfa5cef374a4a59e5881945a97a93eae", + "sha256:45ad7dfaaa7d55cab4cd1e85e03f27e9d60bc067ddc59db52a2b0aeca8870292" ], "index": "pypi", - "version": "==0.12.1" + "version": "==0.14.0" + }, + "websocket-client": { + "hashes": [ + "sha256:4cf754af7e3b3ba76589d49f9e09fd9a6c0aae9b799a89124d656009c01a261d", + "sha256:8d07f155f8ed14ae3ced97bd7582b08f280bb1bfd27945f023ba2aceff05ab52" + ], + "index": "pypi", + "version": "==1.1.1" }, "websockets": { "hashes": [ @@ -276,6 +297,7 @@ "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" ], + "index": "pypi", "version": "==0.6.1" }, "pycodestyle": { @@ -283,7 +305,7 @@ "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "index": "pypi", "version": "==2.7.0" }, "pyflakes": { @@ -291,7 +313,7 @@ "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "index": "pypi", "version": "==2.3.1" } } diff --git a/README.md b/README.md index 907f7d47..b2e1fd8e 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ # Morelia Server - мессенджер (сервер) для Morelia Network # -![]() *тут скриншот* +Language [EN](./README_ENG.md), [RU](./README.md) ## В репозитории 2 бранча ## -[master](https://github.com/MoreliaTalk/morelia_server/tree/master) - Основная и стабильная ветка. +[master](https://github.com/MoreliaTalk/morelia_server/tree/master) - стабильная ветка. -[master-develop](https://github.com/MoreliaTalk/morelia_server/tree/develop) - Ветка для добавления нового функционала. +[develop](https://github.com/MoreliaTalk/morelia_server/tree/develop) - ветка для добавления нового функционала. ## В разработке применяется ## -* [Python 3.8](https://www.python.org/) - язык программирования +* [Python 3.9](https://www.python.org/) - язык программирования * [FastAPI](https://fastapi.tiangolo.com) - основной фреймворк @@ -18,41 +18,38 @@ * [Pydantic](https://pydantic-docs.helpmanual.io) - валидация данных -* [Starlette](https://www.starlette.io) - лёгковесный ASGI фреймворк/тулкит. +* [Starlette](https://www.starlette.io) - лёгковесный ASGI фреймворк/тулкит + +* [websockets](https://pypi.org/project/websockets/) - реализация протокола Websockets в Python (RFC 6455 & 7692) ## Описание репозитория ## * /mod - * api.py - модуль отвечает за описание АПИ, а так же валидацию данных. - * config.py - модуль отвечает за хранение настроек (констант). - * controller.py - модуль отвечает за реализацию методов описанных в [Morelia Protocol](https://github.com/MoreliaTalk/morelia_protocol/blob/master/README.md) + * api.py - модуль отвечает за описание API, а так же валидацию данных. + * error.py - модуль отвечает за хранение кодов ошибок. + * controller.py - модуль отвечает за реализацию методов описанных в [Morelia Protocol](https://github.com/MoreliaTalk/morelia_protocol/blob/master/README.md). * lib.py - модуль отвечает за хэширования пароля, сравнения пароля с его хэш-суммой, создание хэша для auth_id. * models.py - модуль отвечает за описание таблиц БД для работы через ОРМ. - -* /templates - шаблоны для вывода статистики сервера в браузере - * base.html - базовый шаблон с основными элементами меню, он имплементируется в каждый рабочий шаблон - * index.html - рабочий шаблон главной страницы - * status.thml - рабочий шаблон страницы со статусом работы сервера - -* /settings - * logging.py - настройки логирования - + * logging.py - модуль настройки логирования. +* /templates - шаблоны для вывода статистики сервера в браузере. + * base.html - базовый шаблон с основными элементами меню, он имплементируется в каждый рабочий шаблон. + * index.html - рабочий шаблон главной страницы. + * status.thml - рабочий шаблон страницы со статусом работы сервера. * server.py - основной код сервера - -* manage.py - менеджер миграции для БД - +* manage.py - менеджер миграции для БД (создание и удаление таблиц базы данных) * /tests * fixtures/ - * api.json - json-файл с заранее подготовленными данными, для провдедения тестов - * test_api.py - тесты для проверки валидации - * test_controller.py - тесты для проверки класса который отвечает за обработкуметодов протокола - * test_lib.py - тесты хэш-функции - -* debug_server.py - обёртка для server.py для дебага через утилиту `pdb` + * api.json - json-файл с заранее подготовленными данными, для провдедения тестов. + * test_api.py - тесты для проверки валидации. + * test_controller.py - тесты для проверки класса который отвечает за обработкуметодов протокола. + * test_lib.py - тесты хэш-функции. +* debug_server.py - обёртка для server.py для дебага через утилиту `pdb`. +* example_config.ini - файл содержащий пример настроек сервера, перед запуском сервера просто переименуйте в `config.ini`. +* client.py - мини-клиент для проверки работы сервера. ## Установка ## -Установить [Python](https://www.python.org/downloads/) версии 3.8. +Установить [Python](https://www.python.org/downloads/) версией 3.8 или выше. Загрузить и установить последнюю версию [git](https://git-scm.com/downloads). @@ -99,7 +96,7 @@ git remote -v ## Настройка виртуального окружения Pipenv ## -Для работы с проектом необходима установка библиотек которые он использует, т.н. `рабочее окружение`, для этого используется утилита [Pipenv](https://github.com/pypa/pipenv) +Для работы с проектом необходимо установить библиотеки которые он использует и настроить т.н. `виртуальное рабочее окружение` или `virtualenv`, для этого используется утилита [Pipenv](https://github.com/pypa/pipenv) Если не установлен pipenv, выполнить @@ -119,27 +116,43 @@ pipenv shell pipenv install --ignore-pipfile ``` -## Запуск сервера ## +## Перед запуском сервера - используем менеджер настроек ## -Перед запуском необходимо создать базу данных с пустыми таблицами, командой +Перед запуском сервера необходимо выполнить некоторые настройки (создать БД, таблицы и добавить первого пользователя - администратора) + +Создаём базу данных с пустыми таблицами: ```cmd -python ./manage.py --table create +pipenv run python ./manage.py --db create ``` -Дополнительно можно добавить первого пользователя в таблицу +Если необходимо удалить все таблицы в созданной базе данных (ВНИМАНИЕ удаляются только таблицы, БД не удаляется): + +```cmd +pipenv run pipenv run python ./manage.py --db delete +``` + +Добавляем администратора в созданную БД: ```cmd python ./manage.py --table superuser ``` -Для дополнительной информации о возможностях менеджера миграций +Дополнительно можно создать `flow` с типом группа: ```cmd -python ./manage.py --help +pipenv run python ./manage.py --table flow ``` -Для запуска сервера используйте команду +Информация о всех возможностях менеджера настроек: + +```cmd +pipenv run python ./manage.py --help +``` + +## Запуск сервера ## + +Для запуска сервера используйте команду: ```cmd uvicorn server:app --host 0.0.0.0 --port 8000 --reload --use-colors --http h11 --ws websockets @@ -181,6 +194,31 @@ uvicorn server:app --host 0.0.0.0 --port 8000 --reload --use-colors --http h11 - `--timeout-keep-alive ` - Close Keep-Alive connections if no new data is received within this timeout. Default: 5. +## Запуск сервера в режиме DEBUG ## + +Для лёгкого запуска сервера в режиме отладки нужно всего лишь запустить `debug_server.py`: + +```cmd +pipenv run python ./debug_server.py +``` + +## Проверка работоспособности сервера с помощью встроенного клиента ## + +Для проверки работы сервера запустите мини-клиент `client.py` в консоли: + +```cmd +pipenv run python -i ./client.py +``` + +После запуска клиент отправит на сервер сообщение об авторизации (AUTH), ответ сервера будет выведен в консоль, после чего `python` перейдёт в режим интерактивной строки `>>>`, для того чтобы была возможность провести дополнительные проверки. + +В интерактивной консоли будет доступна одна функция отправки сообщений `send_message` которая принимает два аргумента `message`-сообщение и `uri`-адрес сервера. В аргументе `message` необходимо передать объект с типом "dict" или "str", можно использовать готовые примеры запросов: AUTH, GET_UPDATE, ADD_FLOW, ALL_FLOW. В аргументе необходимо передать объект с типом "str", можно использовать готовый пример адреса сервера: LOCALHOST. + +```py +>>> send_message(GET_UPDATE, LOCALHOST) +``` + +Если в функцию не передать ни одного аргумента, по умолчанию будет отправлено сообщение AUTH на LOCALHOST. ## Создание пулл-реквеста для внесенния изменений в develop-ветку Morelia Server ## @@ -196,7 +234,7 @@ git pull upstream develop git push ``` -Для создания пулл-реквеста, необходимо перейти на [GitHub](https://www.github.com), выбрать свой форк и в правом меню нажать на `New pull request`, после чего выбрать бранч из которого будет производится перенос изменений в develop-ветку Morelia Qt и нажать `Create pull request`. +Для создания пулл-реквеста, необходимо перейти на [GitHub](https://www.github.com), выбрать свой форк и в правом меню нажать на `New pull request`, после чего выбрать бранч из которого будет производится перенос изменений в develop-ветку Morelia Server и нажать `Create pull request`. ## Требования к стилю кода ## @@ -216,15 +254,10 @@ INFO | logger.info() SUCCESS | logger.success() WARNING | logger.warning() ERROR | logger.error() + | logger.exception() CRITICAL | logger.critical() ``` -Для включения **DEBUG** режима, запускать сервер с параметром: - -```cmd -uvicorn server:app --log-level debug -``` - ## Написание и запуск тестов ## Для написания тестов используется встроенный модуль Unittest. @@ -232,7 +265,7 @@ uvicorn server:app --log-level debug Для запуска тестов выполните (вместо звёздочки подставьте наименование теста) ```cmd -python -v ./tests/test_*.py +pipenv run python -v ./tests/test_*.py ``` ## Запуск дебаггера ## diff --git a/README_ENG.md b/README_ENG.md new file mode 100644 index 00000000..ecb3fba3 --- /dev/null +++ b/README_ENG.md @@ -0,0 +1,289 @@ +# Morelia Server - messenger server for Morelia Network # + +Language [EN](./README_ENG.md), [RU](./README.md) + +## There are two brunches in repository ## + +[master](https://github.com/MoreliaTalk/morelia_server/tree/master) - stable branch. + +[develop](https://github.com/MoreliaTalk/morelia_server/tree/develop) - branch to add new functionality. + +## Development applies ## + +* [Python 3.9](https://www.python.org/) - programming language + +* [FastAPI](https://fastapi.tiangolo.com) - basic framework + +* [SQLObject](http://sqlobject.org) - ORM for working with the database + +* [Pydantic](https://pydantic-docs.helpmanual.io) - data validation + +* [Starlette](https://www.starlette.io) - lightweight ASGI framework + +* [websockets](https://pypi.org/project/websockets/) - Python implementation of Websockets protocol (RFC 6455 & 7692) + +## Repository description ## + +* /mod + * api.py - module is responsible for description of API, as well as validation of data. + * error.py - module is responsible for storing error codes. + * controller.py - module is responsible for implementing methods described in [Morelia Protocol](https://github.com/MoreliaTalk/morelia_protocol/blob/master/README.md). + * lib.py - module is responsible for hashing password, comparing password with its hash sum, and creating a hash for auth_id. + * models.py - module is responsible for describing database tables to work through ORM. + * logging.py - logging configuration module. +* /templates - templates for displaying server statistics in browser. + * base.html - base template with basic elements of menu, it is implemented in every working template. + * index.html - working homepage template. + * status.thml - working page template with status of server. +* server.py - basic server code. +* manage.py - migration manager for database (creating and deleting database tables). +* /tests + * fixtures/ + * api.json - json-file with pre-prepared data, to conduct tests. + * test_api.py - validation tests. + * test_controller.py - tests to check class that is responsible for processing protocol methods. + * test_lib.py - hash function tests. +* debug_server.py - A wrapper for server.py to debug through a utility `pdb`. +* example_config.ini - file containing the example server settings, just rename it to `config.ini` before starting the server. +* client.py - mini client to check the server operation. + +## Installing ## + +Install [Python](https://www.python.org/downloads/) version 3.8 or higher. + +Download and install latest version [git](https://git-scm.com/downloads). + +If you need a GUI, install [GitHub Desktop](https://desktop.github.com/). + +Set up Git or GitHub Desktop by entering your `username` and `email` from an account created on [github](https://www.github.com). + +## Fork repository Morelia Server ## + +If you are not on the GitHub project team, you must first fork the Morelia Server repository to yourself by going to [link](https://github.com/MoreliaTalk/morelia_server/fork). + +## Cloning a repository to a local computer ## + +Clone repository to your local computer using command line and `git` + +```cmd +git clone https://github.com/{username}/morelia_server.git +cd morelia_server +``` + +Switching to `develop` branch + +```cmd +git checkout develop +``` + +Synchronizing your fork with original repository `upstream` Morelia Server + +```cmd +git remote add upstream https://github.com/MoreliaTalk/morelia_server.git +``` + +Check if repository `upstream` appeared in the list of deleted repositories + +```cmd +git remote -v +> origin https://github.com/{username}/morelia_server.git (fetch) +> origin https://github.com/{username}/morelia_server.git (push) +> upstream https://github.com/MoreliaTalk/morelia_server.git (fetch) +> upstream https://github.com/MoreliaTalk/morelia_server.git (push) +``` + +If using `GitHub`, select `Clone repository...` from `File` menu and follow instructions + +## Configuring the Pipenv Virtual Environment ## + +To work with the project it is necessary to install the libraries that it uses and configure the so-called `virtual environment` or `virtualenv`, for this purpose the utility [Pipenv](https://github.com/pypa/pipenv) + +If pipenv is not installed, run + +```cmd +python -m pip install pipenv +``` + +Create a virtual environment in project directory + +```cmd +pipenv shell +``` + +Install all required libraries from Pipfile + +```cmd +pipenv install --ignore-pipfile +``` + +## Before starting the server - use the configuration manager ## + +Before you start the server you need to make some settings (create a database, tables and add the first user - administrator) + +Create a database with empty tables: + +```cmd +pipenv run python ./manage.py --db create +``` + +If you want to delete all tables in the created database (WARNING only tables are deleted, the database is not deleted): + +```cmd +pipenv run python ./manage.py --db delete +``` + +Add the administrator in the created database: + +```cmd +pipenv run python ./manage.py --table superuser +``` + +Additionally, you can create a `flow` with the group type: + +```cmd +pipenv run python ./manage.py --table flow +``` + +Information about all the features of the configuration manager: + +```cmd +pipenv run python ./manage.py --help +``` + +## Server startup ## + +To start server, use command: + +```cmd +uvicorn server:app --host 0.0.0.0 --port 8000 --reload --use-colors --http h11 --ws websockets +``` + +Additional parameters that can be sent to server: + +`--log-level ` - Set the log level. Options: 'critical', 'error', 'warning', 'info', 'debug', 'trace'. Default: 'info'. + +`--use-colors / --no-use-colors` - Enable / disable colorized formatting of the log records, in case this is not set it will be auto-detected. This option is ignored if the `--log-config` CLI option is used. + +`--loop ` - Set the event loop implementation. The uvloop implementation provides greater performance, but is not compatible with Windows or PyPy. Options: 'auto', 'asyncio', 'uvloop'. Default: 'auto'. + +`--http ` - Set the HTTP protocol implementation. The httptools implementation provides greater performance, but it not compatible with PyPy, and requires compilation on Windows. Options: 'auto', 'h11', 'httptools'. Default: 'auto'. + +`--ws ` - Set the WebSockets protocol implementation. Either of the websockets and wsproto packages are supported. Use 'none' to deny all websocket requests. Options: 'auto', 'none', 'websockets', 'wsproto'. Default: 'auto'. + +`--lifespan ` - Set the Lifespan protocol implementation. Options: 'auto', 'on', 'off'. Default: 'auto'. + +`--interface` - Select ASGI3, ASGI2, or WSGI as the application interface. Note that WSGI mode always disables WebSocket support, as it is not supported by the WSGI interface. Options: 'auto', 'asgi3', 'asgi2', 'wsgi'. Default: 'auto'. + +`--limit-concurrency ` - Maximum number of concurrent connections or tasks to allow, before issuing HTTP 503 responses. Useful for ensuring known memory usage patterns even under over-resourced loads. + +`--limit-max-requests ` - Maximum number of requests to service before terminating the process. Useful when running together with a process manager, for preventing memory leaks from impacting long-running processes. + +`--backlog ` - Maximum number of connections to hold in backlog. Relevant for heavy incoming traffic. Default: 2048 + +`--ssl-keyfile ` - SSL key file + +`--ssl-certfile ` - SSL certificate file + +`--ssl-version ` - SSL version to use (see stdlib ssl module's) + +`--ssl-cert-reqs ` - Whether client certificate is required (see stdlib ssl module's) + +`--ssl-ca-certs ` - CA certificates file + +`--ssl-ciphers ` - Ciphers to use (see stdlib ssl module's) + +`--timeout-keep-alive ` - Close Keep-Alive connections if no new data is received within this timeout. Default: 5. + +## Running the server in DEBUG mode ## + +To easily start the server in debug mode, all you need to do is run `debug_server.py`: + +```cmd +pipenv run python ./debug_server.py +``` + +## Checking server availability with the built-in client ## + +To test the server, run the mini client `client.py` in the console: + +```cmd +pipenv run python -i ./client.py +``` + +After launching, the client will send an authorization message (AUTH) to the server, the server's response will be displayed in the console, after which `python` will go into interactive line mode `>>>`, so that it is possible to perform additional checks. + +In the interactive console, there will be one function available to send `end_message` which takes two arguments `message` message and `uri` server address. In the argument `message` you need to pass an object with type "dict" or "str", you can use ready-made examples of queries: AUTH, GET_UPDATE, ADD_FLOW, ALL_FLOW. In the argument you must pass an object with type "str", you can use ready examples of server address: LOCALHOST. + +```py +>>> send_message(GET_UPDATE, LOCALHOST) +``` + +If no arguments are passed to the function, the default is to send an AUTH message to LOCALHOST. + +## Creating a pool-request to make changes to development branch Morelia Server ## + +Getting latest changes from development branch Morelia Server + +```cmd +git pull upstream develop +``` + +Sending changes to development branch of your fork + +```cmd +git push +``` + +To create a pull-request, you need to go to [GitHub](https://www.github.com), select your fork and click on `New pull request` in right-hand menu, then select branch from which you want to push changes to Morelia Server development branch and click `Create pull request`. + +## Code style requirements ## + +Before starting work, it is recommended that you read [PEP 8 - Python Code Writing Guide](https://pythonworld.ru/osnovy/pep-8-rukovodstvo-po-napisaniyu-koda-na-python.html). Be sure to use a linter (flake8, pylint or similar). + +## Logging ## + +Library is used [loguru](https://github.com/Delgan/loguru) + +Logging levels you can use in your code: + +```py +Level name | Logger method + +DEBUG | logger.debug() +INFO | logger.info() +SUCCESS | logger.success() +WARNING | logger.warning() +ERROR | logger.error() + | logger.exception() +CRITICAL | logger.critical() +``` + +## Writing and running tests ## + +Built-in Unittest module is used to write tests. + +To run tests, run (replace asterisk with test name) + +```cmd +pipenv run python -v ./tests/test_*.py +``` + +## Running the debugger ## + +To run debugger in a working server environment, through console + +```cmd +python -m pdb ./debug_server.py +``` + +To get help in debug mode + +```cmd +(pdb) help +``` + +## Контакты ## + +[Telegram](https://t.me/joinchat/LImHShzAmIWvpMxDTr5Vxw) - a group where pressing issues are discussed. + +[Slack](www.moreliatalk.slack.com) - an alternative way of discussing the project. diff --git a/client.py b/client.py new file mode 100644 index 00000000..84ac920a --- /dev/null +++ b/client.py @@ -0,0 +1,135 @@ +# ************** Standart module ********************* +import json +from typing import Union +# ************** Standart module end ***************** + + +# ************** External module ********************* +import websocket +# ************** External module end ***************** + + +# ************** Logging beginning ******************* +from loguru import logger +from mod.logging import add_logging +# ************** Logging end ************************* + + +# logger on INFO +add_logging(20) + +# URL and Port +LOCALHOST = 'ws://localhost:8000/ws' + +# Registration infirmation from superuser +user_login = 'login' +user_password = 'password' +salt = b'salt' +key = b'key' +uuid = '123456789' + +GET_UPDATE = { + "type": "get_update", + "data": { + "time": 111, + "user": [{ + "uuid": uuid, + "auth_id": "auth_id", + }], + "meta": None + }, + "jsonapi": { + "version": "1.0" + }, + "meta": None + } + +AUTH = { + "type": "auth", + "data": { + "user": [{ + "password": user_password, + "login": user_login + }], + "meta": None + }, + "jsonapi": { + "version": "1.0" + }, + "meta": None + } + +ADD_FLOW = { + "type": "add_flow", + "data": { + "flow": [{ + "type": "group", + "title": "title", + "info": "info", + "owner": uuid, + "users": [uuid] + }], + "user": [{ + "uuid": uuid, + "auth_id": "auth_id", + }], + "meta": None + }, + "jsonapi": { + "version": "1.0" + }, + "meta": None + } + +ALL_FLOW = { + "type": "all_flow", + "data": { + "user": [{ + "uuid": uuid, + "auth_id": "auth_id" + }], + "meta": None + }, + "jsonapi": { + "version": "1.0" + }, + "meta": None + } + + +# Chat websocket +def send_message(message: Union[dict, str] = AUTH, uri: str = LOCALHOST) -> bytes: + result = b'None' + """Sending a message via websockets, with a response + + Args: + message ([dict], required): message. + uri ([str], required): server address like 'ws://host:port/ws' + + Returns: + message [bytes]: Response as a byte object. + """ + ws = websocket.WebSocket() + try: + ws.connect(uri) + logger.success("Setting up connection") + except Exception as error: + logger.exception(str(error)) + logger.error("Problems in connection to server") + else: + logger.info(f"Connection status: {ws.getstatus()}") + ws.send(json.dumps(message)) + logger.info("Message send") + result = ws.recv() + logger.info("Server response received") + ws.close(status=1000) + logger.success("Session with server ended successfully") + finally: + ws.close(status=1002) + return result + + +if __name__ == "__main__": + result = send_message() + logger.debug(f"Server response: {json.loads(result)}") + logger.info("END") diff --git a/debug_server.py b/debug_server.py index be7f4036..c9da0437 100644 --- a/debug_server.py +++ b/debug_server.py @@ -2,13 +2,12 @@ import server # noqa - if __name__ == "__main__": uvicorn.run("server:app", host="0.0.0.0", port=8000, http="h11", ws="websockets", - log_level="trace", - use_colors=False, + log_level="debug", + use_colors=True, debug=True) diff --git a/example_config.ini b/example_config.ini new file mode 100644 index 00000000..17eeb09f --- /dev/null +++ b/example_config.ini @@ -0,0 +1,58 @@ +# + +[DATABASE] +# Configuring database access +# When URI is: +# SQLite = sqlite:/C:full/path/to/database/database_name.db +# SQLite = sqlite:/:memory: +# PostgreSQL = postgres://postgres:123456@127.0.0.1/database_name?debug=True +# MySQL = mysql://user:password@host/database +URI = sqlite:/Z:/GoogleDisk/work/morelia_server/db_sqlite.db + + +[HASH_SIZE] +# size of output hash digest in bytes +PASSWORD = 32 +# size of output auth_id digest in bytes +AUTH_ID = 16 + + +[LOGGING] +# Level status: +# 50 - CRITICAL +# 40 - ERROR +# 30 - WARNING +# 25 - SUCCES +# 20 - INFO +# 10 - DEBUG +# 5 - TRACE +LEVEL = 20 + +# Log message formating +DEBUG = "{time:YYYY-MM-DD HH:mm:ss} | {level} | {module} | {function} | line:{line: >3} | {message}" +ERROR = "{time:YYYY-MM-DD HH:mm:ss} | {level} | {module} | {function} | line:{line: >3} | {message}" +INFO = "{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}" + + +[TEMPLATES] +# Settings Jinja2 +FOLDER = templates + + +[SERVER_LIMIT] +# Setting up number of messages that server gives out on +# "get_all_message" client request +MESSAGES = 100 +# Setting up number of users that server gives out on +# "get_user_info" client request +USERS = 100 + + +[SUPERUSER] +UUID = 123456789 +USERNAME = superuser +LOGIN = login +PASSWORD = password +SALT = salt +KEY = key +HASH_PASSWORD = 8b915f2f0b0d0ccf27854dd708524d0b5a91bdcd3775c6d3335f63d015a43ce1 \ No newline at end of file diff --git a/manage.py b/manage.py index 1ef787b6..ed12cc71 100644 --- a/manage.py +++ b/manage.py @@ -1,53 +1,87 @@ import sys -import time -import click +from time import time, process_time import inspect +from uuid import uuid4 +import configparser import sqlobject as orm +import click -from mod import config from mod import models -# Connect to database -connection = orm.connectionForURI(config.LOCAL_POSTGRESQL) -orm.sqlhub.processConnection = connection - -# looking for all Classes listed in the models.py file. -classes = [cls_name for cls_name, cls_obj - in inspect.getmembers(sys.modules['mod.models']) - if inspect.isclass(cls_obj)] +# ************** Read "config.ini" ******************** +config = configparser.ConfigParser() +config.read('config.ini') +logging = config['LOGGING'] +database = config["DATABASE"] +superuser = config["SUPERUSER"] +# ************** END ********************************** @click.command() -@click.option('--table', default='create', help='Create or delete all table \ - whit all data. The operation, by default, creates all tables') -def main(table): - if table == "create": - start_time = time.process_time() +@click.option("--db", + type=click.Choice(["create", "delete"]), + help='Create or delete all table ' + 'with all data. Оperation, ' + 'by default, creates all tables') +@click.option('--table', + type=click.Choice(["superuser", "flow"]), + help='Creating records in the database. ' + 'You can create a superuser or flow type "group".') +def main(db, table): + # Connect to database + connection = orm.connectionForURI(database.get("uri")) + orm.sqlhub.processConnection = connection + + # looking for all Classes listed in models.py + classes = [cls_name for cls_name, cls_obj + in inspect.getmembers(sys.modules['mod.models']) + if inspect.isclass(cls_obj)] + + if db == "create": + start_time = process_time() for item in classes: + # Create tables in database for each class + # that is located in models module class_ = getattr(models, item) class_.createTable(ifNotExists=True) - return print(f"Table is createt at: \ - {time.process_time() - start_time} sec.") - if table == "superuser": - models.User(uuid=123456, - login="login", - password="password", - hashPassword="8b915f2f0b0d0ccf27854dd708524d0b\ - 5a91bdcd3775c6d3335f63d015a43ce1", - username="superuser", - salt=b"salt", - key=b"key") - return print("Create user") - elif table == "delete": - start_time = time.process_time() + click.echo(f'Table is createt at: ' + f'{process_time() - start_time} sec.') + elif db == "delete": + start_time = process_time() for item in classes: class_ = getattr(models, item) class_.dropTable(ifExists=True, dropJoinTables=True, cascade=True) - return print(f"Table is deleted at: \ - {time.process_time() - start_time} sec.") - else: - return print("ERROR. Function \'--table\' did not work") + click.echo(f'Table is deleted at: ' + f'{process_time() - start_time} sec.') + + user_uuid = str(123456789) + hash_password = superuser.get('hash_password') + if table == "superuser": + try: + models.UserConfig(uuid=user_uuid, + login="login", + password="password", + hashPassword=hash_password, + username="superuser", + salt=b"salt", + key=b"key") + click.echo("Superuser created") + except orm.dberrors.OperationalError as error: + click.echo(f'Failed to create a user. Error text: {error}') + elif table == "flow": + try: + new_user = models.UserConfig.selectBy(uuid=user_uuid).getOne() + new_flow = models.Flow(uuid=str(uuid4().hex), + timeCreated=int(time()), + flowType="group", + title="Test", + info="Test flow", + owner=user_uuid) + new_flow.addUserConfig(new_user) + click.echo("Flow created") + except orm.dberrors.OperationalError as error: + click.echo(f'Failed to create a flow. Error text: {error}') if __name__ == "__main__": diff --git a/mod/api.py b/mod/api.py index 253b5150..02d75f54 100644 --- a/mod/api.py +++ b/mod/api.py @@ -6,21 +6,28 @@ from pydantic import BaseModel from pydantic import EmailStr +# Version of MoreliaTalk Protocol +VERSION: str = '1.0' + class Flow(BaseModel): class Config: title = 'List of flow with description and type' - id: Optional[int] = None + uuid: Optional[str] = None time: Optional[int] = None type: Optional[str] = None title: Optional[str] = None info: Optional[str] = None + owner: Optional[str] = None + users: Optional[List] = None + message_start: Optional[int] = None + message_end: Optional[int] = None class User(BaseModel): class Config: title = 'List of user information' - uuid: Optional[int] = None + uuid: Optional[str] = None bio: Optional[str] = None avatar: Optional[bytes] = None password: Optional[str] = None @@ -34,11 +41,12 @@ class Config: class Message(BaseModel): class Config: title = 'List of message information' - id: Optional[int] = None + uuid: Optional[str] = None + client_id: Optional[int] = None text: Optional[str] = None - from_user_uuid: Optional[int] = None + from_user: Optional[str] = None time: Optional[int] = None - from_flow_id: Optional[int] = None + from_flow: Optional[str] = None file_picture: Optional[bytes] = None file_video: Optional[bytes] = None file_audio: Optional[bytes] = None @@ -81,6 +89,7 @@ class Config: errors: Optional[Errors] = None jsonapi: Optional[Version] = None meta: Optional[Any] = None + def toJSON(self): return json.dumps(self, ensure_ascii=False, diff --git a/mod/controller.py b/mod/controller.py index 57b38b6e..0b67be21 100644 --- a/mod/controller.py +++ b/mod/controller.py @@ -1,23 +1,30 @@ -import random -from os import urandom from time import time -from typing import Optional, Union +from typing import Union +from uuid import uuid4 +import configparser from pydantic import ValidationError from sqlobject import AND from sqlobject import SQLObjectIntegrityError from sqlobject import SQLObjectNotFound from sqlobject import dberrors +from loguru import logger from mod import api -from mod import config +from mod import error from mod import lib from mod import models +# ************** Read "config.ini" ******************** +config = configparser.ConfigParser() +config.read('config.ini') +limit = config['SERVER_LIMIT'] +# ************** END ********************************** + class ProtocolMethods: - """The class is responsible for processing requests and forming answers - according to "Udav" protocol. Protocol version and it's actual description: + """Processing requests and forming answers according to "MTP" protocol. + Protocol version and it's actual description: https://github.com/MoreliaTalk/morelia_protocol/blob/master/README.md """ def __init__(self, request): @@ -28,14 +35,16 @@ def __init__(self, request): self.response.data.user = list() self.response.errors = api.Errors() self.response.jsonapi = api.Version() - self.response.jsonapi.version = config.API_VERSION + self.response.jsonapi.version = api.VERSION self.get_time = int(time()) try: self.request = api.ValidJSON.parse_obj(request) - except ValidationError as error: + logger.success("Validation was successful") + except ValidationError as ERROR: self.response.type = "errors" - self.__catching_error(415, str(error)) + self.__catching_error(415, str(ERROR)) + logger.debug(f"Validation failed: {ERROR}") else: self.response.type = self.request.type if self.request.type == 'register_user': @@ -71,14 +80,15 @@ def __init__(self, request): self.__catching_error(401) def get_response(self): - """Function generates a JSON-object containing result + """Generates a JSON-object containing result of an instance of ProtocolMethod class. """ return self.response.toJSON() - def __check_auth_token(self, uuid: str, auth_id: str) -> bool: - """Function checks uuid and auth_id of user + def __check_auth_token(self, uuid: Union[str, int], + auth_id: str) -> bool: + """Checks uuid and auth_id of user Args: uuid (int, requires): Unique User ID @@ -88,20 +98,25 @@ def __check_auth_token(self, uuid: str, auth_id: str) -> bool: True if successful False if unsuccessful """ + if isinstance(uuid, int): + uuid = str(uuid) try: dbquery = models.UserConfig.selectBy(uuid=uuid).getOne() + logger.success("User was found in the database") except (dberrors.OperationalError, SQLObjectIntegrityError, SQLObjectNotFound): + logger.debug("User wasn't found in the database") return False else: if auth_id == dbquery.authId: + logger.success("Authentication token has been verified") return True else: + logger.debug("Authentication token failed") return False def __check_login(self, login: str) -> bool: - """Provides information about all personal settings of user - (in a server-friendly form) + """Checks database for a user with the same login Args: login (str, optional): user login @@ -113,28 +128,29 @@ def __check_login(self, login: str) -> bool: try: models.UserConfig.selectBy(login=login).getOne() except (SQLObjectIntegrityError, SQLObjectNotFound): + logger.debug("There is no user in the database") return False else: + logger.success("User was found in the database") return True def __catching_error(self, code: Union[int, str], - add_info: Optional[str] = None) -> None: - """Function catches errors in the "try...except" content. - Result is 'dict' with information about the code, status, - time and detailed description of the error that has occurred. + add_info: Union[Exception, str] = None) -> None: + """Сatches errors in "try...except" content. + Result is 'dict' with information about code, status, + time and detailed description of error that has occurred. For errors like Exception and other unrecognized errors, code "520" and status "Unknown Error" are used. - Function also automatically logs the error. Args: code (Union[int, str]): Error code or type and exception description. add_info (Optional[str], optional): Additional information - to be added. The 'Exception' field is not used for exceptions. + to be added. 'Exception' field is not used for exceptions. Defaults to None. Returns: - dict: returns 'dict' according to the protocol, + dict: returns 'dict' according to protocol, like: { 'code': 200, 'status': 'Ok', @@ -142,11 +158,13 @@ def __catching_error(self, code: Union[int, str], 'detail': 'successfully' } """ - if code in config.DICT_ERRORS: + if code in error.DICT: if add_info is None: - add_info = config.DICT_ERRORS[code]['detail'] + add_info = error.DICT[code]['detail'] + else: + logger.exception(str(add_info)) self.response.errors.code = code - self.response.errors.status = config.DICT_ERRORS[code]['status'] + self.response.errors.status = error.DICT[code]['status'] self.response.errors.time = self.get_time self.response.errors.detail = add_info else: @@ -154,43 +172,45 @@ def __catching_error(self, code: Union[int, str], self.response.errors.status = 'Unknown Error' self.response.errors.time = self.get_time self.response.errors.detail = code + logger.debug(f"Status code({code}): {error.DICT[code]['status']}") def _register_user(self): - """The function registers the user who is not in the database. + """Registers user who is not in the database. Note: This version also authentificate user, that exist in database - Future version will return error if login exist in database """ - # FIXME после замены uuid на UUID из питоньего модуля - random.seed(urandom(64)) - gen_uuid = random.randrange(10000, 999999999999) - if self.__check_login(self.request.data.user[0].login): + uuid = str(uuid4().int) + password = self.request.data.user[0].password + login = self.request.data.user[0].login + username = self.request.data.user[0].username + email = self.request.data.user[0].email + if self.__check_login(login): self.__catching_error(409) else: - generated = lib.Hash(password=self.request.data.user[0].password, - uuid=gen_uuid) - models.UserConfig(uuid=gen_uuid, - password=self.request.data.user[0].password, + generated = lib.Hash(password=password, + uuid=uuid) + auth_id = generated.auth_id() + models.UserConfig(uuid=uuid, + password=password, hashPassword=generated.password_hash(), - login=self.request.data.user[0].login, - username=self.request.data.user[0].username, - email=self.request.data.user[0].email, + login=login, + username=username, + email=email, key=generated.get_key(), salt=generated.get_salt(), - authId=(gen_auth_id := generated.auth_id())) + authId=auth_id) user = api.User() - user.uuid = gen_uuid - user.auth_id = gen_auth_id + user.uuid = uuid + user.auth_id = auth_id self.response.data.user.append(user) + logger.success("User is registred") self.__catching_error(201) def _get_update(self): - """The function displays messages of a specific flow, - from the timestamp recorded in the request to the server timestamp, - retrieves them from the database - and issues them as an array consisting of JSON + """Provides updates of flows, messages and users in them from time "time" """ + # select all fields of the user table # TODO внеести измнения в протокол, добавить фильтр # по дате создания пользователя dbquery_user = models.UserConfig.selectBy() @@ -201,27 +221,37 @@ def _get_update(self): if dbquery_message.count(): for element in dbquery_message: message = api.Message() + message.uuid = element.uuid message.text = element.text + message.from_user = element.user.uuid message.time = element.time - message.emoji = element.emoji + message.from_flow = element.flow.uuid message.file_picture = element.filePicture message.file_video = element.fileVideo message.file_audio = element.fileAudio message.file_document = element.fileDocument - message.from_user_uuid = element.userID - message.from_flow_id = element.flowID + message.emoji = element.emoji + message.edited_time = element.editedTime message.edited_status = element.editedStatus self.response.data.message.append(message) - elif dbquery_flow.count(): + else: + self.response.data.message + + if dbquery_flow.count(): for element in dbquery_flow: flow = api.Flow() - flow.id = element.flowId + flow.uuid = element.uuid flow.time = element.timeCreated flow.type = element.flowType flow.title = element.title flow.info = element.info + flow.owner = element.owner + flow.users = [item.uuid for item in element.users] self.response.data.flow.append(flow) - elif dbquery_user.count(): + else: + self.response.data.flow + + if dbquery_user.count(): for element in dbquery_user: user = api.User() user.uuid = element.uuid @@ -231,77 +261,168 @@ def _get_update(self): user.bio = element.bio self.response.data.user.append(user) else: - self.__catching_error(404) + self.response.data.user + logger.success("\'_get_update\' executed successfully") self.__catching_error(200) def _send_message(self): - """The function saves user message in the database. + """Saves user message in database. """ + message_uuid = str(uuid4().int) + flow_uuid = self.request.data.flow[0].uuid + text = self.request.data.message[0].text + picture = self.request.data.message[0].file_picture + video = self.request.data.message[0].file_video + audio = self.request.data.message[0].file_audio + document = self.request.data.message[0].file_document + emoji = self.request.data.message[0].emoji + user_uuid = self.request.data.user[0].uuid try: - models.Flow.selectBy(flowId=self.request.data.flow[0].id).getOne() - except SQLObjectNotFound as flow_error: - self.__catching_error(404, str(flow_error)) + flow = models.Flow.selectBy(uuid=flow_uuid).getOne() + user = models.UserConfig.selectBy(uuid=user_uuid).getOne() + except SQLObjectNotFound as ERROR: + self.__catching_error(404, str(ERROR)) else: - models.Message(text=self.request.data.message[0].text, + models.Message(uuid=message_uuid, + text=text, time=self.get_time, - filePicture=self.request.data.message[0].file_picture, - fileVideo=self.request.data.message[0].file_video, - fileAudio=self.request.data.message[0].file_audio, - fileDocument=self.request.data.message[0].file_audio, - emoji=self.request.data.message[0].emoji, - editedTime=self.request.data.message[0].edited_time, - editedStatus=self.request.data. - message[0].edited_status, - userConfig=self.request.data.user[0].uuid, - flow=self.request.data.flow[0].id) + filePicture=picture, + fileVideo=video, + fileAudio=audio, + fileDocument=document, + emoji=emoji, + editedTime=None, + editedStatus=False, + user=user, + flow=flow) + message = api.Message() + message.client_id = self.request.data.message[0].client_id + message.from_flow = flow_uuid + message.from_user = user_uuid + message.uuid = message_uuid + self.response.data.message.append(message) + logger.success("\'_send_message\' executed successfully") self.__catching_error(200) + def _all_messages(self): + """Displays all messages of a specific flow retrieves them + from database and issues them as an array consisting of JSON + + """ + flow = api.Flow() + flow_uuid = self.request.data.flow[0].uuid + message_start = self.request.data.flow[0].message_start + message_end = self.request.data.flow[0].message_end + if message_start is None: + message_start = 0 + if message_end is None: + message_end = 0 + message_volume = message_end - message_start + + def get_messages(db, end: int, start: int = 0) -> None: + for element in db[start:end]: + message = api.Message() + message.from_flow = element.flow.uuid + message.from_user = element.user.uuid + message.text = element.text + message.time = element.time + message.file_picture = element.filePicture + message.file_video = element.fileVideo + message.file_audio = element.fileAudio + message.file_document = element.fileDocument + message.emoji = element.emoji + message.edited_time = element.editedTime + message.edited_status = element.editedStatus + self.response.data.message.append(message) + + try: + flow_dbquery = models.Flow.selectBy(uuid=flow_uuid).getOne() + dbquery = models.Message.select( + AND(models.Message.q.flow == flow_dbquery, + models.Message.q.time >= self.request.data.time)) + MESSAGE_COUNT: int = dbquery.count() + dbquery[0] + except SQLObjectIntegrityError as flow_error: + self.__catching_error(520, str(flow_error)) + except (IndexError, SQLObjectNotFound) as flow_error: + self.__catching_error(404, str(flow_error)) + else: + if MESSAGE_COUNT <= limit.getint("messages"): + self.response.data.flow.append(flow) + get_messages(dbquery, limit.getint("messages")) + logger.success("\'_all_messages\' executed successfully") + self.__catching_error(200) + elif MESSAGE_COUNT > limit.getint("messages"): + flow.message_end = MESSAGE_COUNT + self.response.data.flow.append(flow) + if message_volume <= limit.getint("messages"): + get_messages(dbquery, + self.request.data.flow[0].message_end, + self.request.data.flow[0].message_start) + logger.success("\'_all_messages\' executed successfully") + self.__catching_error(206) + else: + self.__catching_error(403, "Requested more messages" + f" than server limit" + f" (<{limit.getint('messages')})") + def _add_flow(self): - """Function allows you to add a new flow to the database + """Allows add a new flow to database """ - # FIXME после замены flowId на UUID из питоньего модуля - random.seed(urandom(64)) - flow_id = random.randrange(1, 999999) - if self.request.data.flow[0].type not in ["chat", "group", "channel"]: + flow_uuid = str(uuid4().hex) + owner = self.request.data.flow[0].owner + users = self.request.data.flow[0].users + flow_type = self.request.data.flow[0].type + if flow_type not in ["chat", "group", "channel"]: self.__catching_error(400, "Wrong flow type") - elif self.request.data.flow[0].type == 'chat' and len(self.request.data.user) < 2: - self.__catching_error(400, "Two users UUID must be specified for chat") + elif flow_type == 'chat' and len(users) != 2: + self.__catching_error(400, "Must be two users only") else: try: - models.Flow(flowId=flow_id, - timeCreated=self.get_time, - flowType=self.request.data.flow[0].type, - title=self.request.data.flow[0].title, - info=self.request.data.flow[0].info) + dbquery = models.Flow(uuid=flow_uuid, + timeCreated=self.get_time, + flowType=flow_type, + title=self.request.data.flow[0].title, + info=self.request.data.flow[0].info, + owner=owner) + for user_uuid in users: + user = models.UserConfig.selectBy(uuid=user_uuid).getOne() + dbquery.addUserConfig(user) except SQLObjectIntegrityError as flow_error: self.__catching_error(520, str(flow_error)) else: flow = api.Flow() - flow.id = flow_id + flow.uuid = flow_uuid flow.time = self.get_time flow.type = self.request.data.flow[0].type flow.title = self.request.data.flow[0].title flow.info = self.request.data.flow[0].info + flow.owner = owner + flow.users = users self.response.data.flow.append(flow) + logger.success("\'_add_flow\' executed successfully") self.__catching_error(200) def _all_flow(self): - """Function allows to get a list of all flows and - information about them from the database + """Allows to get a list of all flows and information about them + from database """ - dbquery = models.Flow.select(models.Flow.q.flowId >= 1) + dbquery = models.Flow.selectBy() if dbquery.count(): for element in dbquery: flow = api.Flow() - flow.id = element.flowId + flow.uuid = element.uuid flow.time = element.timeCreated flow.type = element.flowType flow.title = element.title flow.info = element.info + flow.owner = element.owner + flow.users = [item.uuid for item in element.users] self.response.data.flow.append(flow) + logger.success("\'_all_flow\' executed successfully") self.__catching_error(200) else: self.__catching_error(404) @@ -310,21 +431,27 @@ def _user_info(self): """Provides information about all personal settings of user. """ - try: - dbquery = models.UserConfig.selectBy(uuid=self.request.data.user[0].uuid).getOne() - except (SQLObjectIntegrityError, SQLObjectNotFound) as user_info_error: - self.__catching_error(404, str(user_info_error)) - else: - user = api.User() - user.uuid = dbquery.uuid - user.login = dbquery.login - user.username = dbquery.username - user.is_bot = dbquery.isBot - user.email = dbquery.email - user.avatar = dbquery.avatar - user.bio = dbquery.bio - self.response.data.user.append(user) + users_volume = len(self.request.data.user) + if users_volume <= limit.getint("users"): + for element in self.request.data.user[1:]: + try: + dbquery = models.UserConfig.selectBy(uuid=element.uuid).getOne() + except SQLObjectIntegrityError as user_info_error: + self.__catching_error(520, str(user_info_error)) + else: + user = api.User() + user.uuid = dbquery.uuid + user.login = dbquery.login + user.username = dbquery.username + user.is_bot = dbquery.isBot + user.avatar = dbquery.avatar + user.bio = dbquery.bio + self.response.data.user.append(user) + logger.success("\'_user_info\' executed successfully") self.__catching_error(200) + else: + self.__catching_error(403, f"Requested more {limit.get('users')}" + " users than server limit") def _authentification(self): """Performs authentification of registered client, @@ -333,11 +460,17 @@ def _authentification(self): and password contained in server database are verified. """ - if self.__check_login(self.request.data.user[0].login) is False: + login = self.request.data.user[0].login + password = self.request.data.user[0].password + if self.__check_login(login) is False: self.__catching_error(404) else: - dbquery = models.UserConfig.selectBy(login=self.request.data.user[0].login).getOne() - generator = lib.Hash(password=self.request.data.user[0].password, + dbquery = models.UserConfig.selectBy(login=login).getOne() + # to check password, we use same module as for its + # hash generation. Specify password entered by user + # and hash of old password as parameters. + # After that, hashes are compared using "check_password" method. + generator = lib.Hash(password=password, uuid=dbquery.uuid, salt=dbquery.salt, key=dbquery.key, @@ -348,42 +481,67 @@ def _authentification(self): user.uuid = dbquery.uuid user.auth_id = dbquery.authId self.response.data.user.append(user) + logger.success("\'_authentification\' executed successfully") self.__catching_error(200) else: self.__catching_error(401) def _delete_user(self): - """Function irretrievably deletes the user from the database. + """Function irretrievably deletes the user from database. """ + uuid = str(uuid4().int) + login = self.request.data.user[0].login + password = self.request.data.user[0].password try: - dbquery = models.UserConfig.selectBy(login=self.request.data.user[0].login, - password=self.request.data.user[0].password).getOne() + dbquery = models.UserConfig.selectBy(login=login, + password=password).getOne() except (SQLObjectIntegrityError, SQLObjectNotFound) as not_found: self.__catching_error(404, str(not_found)) else: - dbquery.delete(dbquery.id) + dbquery.login = "User deleted" + dbquery.password = uuid + dbquery.hashPassword = uuid + dbquery.username = "User deleted" + dbquery.authId = uuid + dbquery.email = "" + dbquery.avatar = b"" + dbquery.bio = "deleted" + dbquery.salt = b"deleted" + dbquery.key = b"deleted" + logger.success("\'_delete_user\' executed successfully") self.__catching_error(200) def _delete_message(self): - """Function deletes the message from the database Message table by its ID. + """Function deletes the message from database Message + table by its ID. """ + uuid = self.request.data.message[0].uuid try: - dbquery = models.Message.selectBy(id=self.request.data.message[0].id).getOne() + dbquery = models.Message.selectBy(uuid=uuid).getOne() except (SQLObjectIntegrityError, SQLObjectNotFound) as not_found: self.__catching_error(404, str(not_found)) else: - dbquery.delete(dbquery.id) + dbquery.text = "Message deleted" + dbquery.filePicture = b'' + dbquery.fileVideo = b'' + dbquery.fileAudio = b'' + dbquery.fileDocument = b'' + dbquery.emoji = b'' + dbquery.editedTime = self.get_time + dbquery.editedStatus = True + logger.success("\'_delete_message\' executed successfully") self.__catching_error(200) def _edited_message(self): - """Function changes the text and time in the database Message table. - The value of the editedStatus column changes from None to True. + """Changes text and time in database Message table. + Value of editedStatus column changes from None to True. """ + uuid = self.request.data.message[0].uuid try: - dbquery = models.Message.selectBy(id=self.request.data.message[0].id).getOne() + dbquery = models.Message.selectBy(uuid=uuid).getOne() except (SQLObjectIntegrityError, SQLObjectNotFound) as not_found: self.__catching_error(404, str(not_found)) else: @@ -391,46 +549,22 @@ def _edited_message(self): dbquery.text = self.request.data.message[0].text dbquery.editedTime = self.get_time dbquery.editedStatus = True + logger.success("\'_edited_message\' executed successfully") self.__catching_error(200) - def _all_messages(self): - """Function displays all messages of a specific flow retrieves them - from the database and issues them as an array consisting of JSON - - """ - dbquery = models.Message.select( - AND(models.Message.q.flowID == self.request.data.flow[0].id, - models.Message.q.time >= self.request.data.time)) - if dbquery.count(): - for element in dbquery: - message = api.Message() - message.from_flow_id = element.flowID - message.from_user_uuid = element.userID - message.text = element.text - message.time = element.time - message.file_picture = element.filePicture - message.file_video = element.fileVideo - message.file_audio = element.fileAudio - message.file_document = element.fileDocument - message.emoji = element.emoji - message.edited_time = element.editedTime - message.edited_status = element.editedStatus - self.response.data.message.append(message) - self.__catching_error(200) - else: - self.__catching_error(404) - def _ping_pong(self): - """The function generates a response to a client's request - for communication between the server and the client. + """Generates a response to a client's request + for communication between server and client. """ + logger.success("\'_ping_pong\' executed successfully") self.__catching_error(200) def _errors(self): - """Function handles cases when a request to server is not recognized by it. - You get a standard answer type: error, which contains an object - with a description of the error. + """Handles cases when a request to server is not recognized by it. + Get a standard answer type: error, which contains an object + with a description of error. """ + logger.success("\'_errors\' executed successfully") self.__catching_error(405) diff --git a/mod/config.py b/mod/error.py similarity index 78% rename from mod/config.py rename to mod/error.py index 4256d6f3..ee3c815d 100644 --- a/mod/config.py +++ b/mod/error.py @@ -1,36 +1,4 @@ -import os - -# Configuring database access -# SQLite3 -LOCAL_SQLITE = ''.join(['sqlite:', os.path.abspath('db_sqlite.db')]) - -# Local PostgreSQL -LOCAL_POSTGRESQL = 'postgres://postgres:123456@127.0.0.1/morelia_server_db?debug=True' - -# Online PostgreSQL -ONLINE_POSTGRESQL = os.getenv('DATABASE_URL') - -# Version of Morelia Protocol -API_VERSION = '1.0' - -# LibHash config # -# TODO -# add constat for configurating iteration cycle - -# size of output hash digest in bytes -PASSWORD_HASH_SIZE = 32 - -# size of output auth_id digest in bytes -AUTH_ID_HASH_SIZE = 16 - -# Settings loguru -DEBUG_LEVEL = 10 - -# Settings Jinja2 - -TEMPLATE_FOLDER = 'templates' - -DICT_ERRORS = { +DICT: dict = { 200: { 'status': 'OK', 'detail': 'successfully' @@ -43,6 +11,10 @@ 'status': 'Accepted', 'detail': 'Accepted' }, + 206: { + 'status': "Partial Content", + 'detail': "Partial Content" + }, 400: { 'status': 'Bad Request', 'detail': 'Bad Request' @@ -99,6 +71,10 @@ 'status': 'Service Unavailable', 'detail': 'Service Unavailable' }, + 520: { + 'status': 'Unknown Error', + 'detail': 'Unknown Error' + }, 526: { 'status': 'Invalid SSL Certificate', 'detail': 'Invalid SSL Certificate' diff --git a/mod/lib.py b/mod/lib.py index 83a438ab..e5ee3eac 100644 --- a/mod/lib.py +++ b/mod/lib.py @@ -2,32 +2,40 @@ from hashlib import blake2b from hmac import compare_digest from os import urandom +from typing import Union +import configparser -from mod import config +# ************** Read "config.ini" ******************** +config = configparser.ConfigParser() +config.read('config.ini') +hash_size = config['HASH_SIZE'] +# ************** END ********************************** class Hash: - """Сlass generates password hashes, hashes for sessions, + """Generates password hashes, hashes for sessions, authenticator ID's, checks passwords hashes. Args: password (str, required): password. - uuid (int, required): unique user identity. + uuid (int or str, [str convert to int], required): unique user + identity. salt (Any, required): Salt. additional unique identifier (there can be any line: mother's maiden name, favorite writer, etc.). key (Any, optional): Additional argument. Defaults to None. - If the value of the 'key' parameter is 'None' then the function + If value of 'key' parameter is 'None' then function will generated it. hash_password (str, optional): password hash (previously calculated). """ - def __init__(self, password: str, uuid: int, salt: bytes = None, - key: bytes = None, hash_password: str = None): + def __init__(self, password: str, uuid: Union[int, str], + salt: bytes = None, key: bytes = None, + hash_password: str = None): if salt is None: self.salt = urandom(16) @@ -39,11 +47,15 @@ def __init__(self, password: str, uuid: int, salt: bytes = None, else: self.key = key + if isinstance(uuid, str): + self.uuid = int(uuid) + else: + self.uuid = uuid + self.binary_password = password.encode('utf-8') - self.uuid = uuid self.hash_password = hash_password - self.size_password = config.PASSWORD_HASH_SIZE - self.size_auth_id = config.AUTH_ID_HASH_SIZE + self.size_password = hash_size.getint('password') + self.size_auth_id = hash_size.getint('auth_id') def get_salt(self) -> bytes: return self.salt @@ -52,7 +64,7 @@ def get_key(self) -> bytes: return self.key def password_hash(self) -> str: - """Function generates a password hash. + """Generates a password hash. Returns: str: Returns hash password. @@ -63,7 +75,7 @@ def password_hash(self) -> str: return hash_password.hexdigest() def check_password(self) -> bool: - """Function checks the password hash and original password. + """Checks password hash and original password. Returns: bool: True or False @@ -76,8 +88,8 @@ def check_password(self) -> bool: verified_hash_password) def auth_id(self) -> str: - """Function generates an authenticator ID's for the client session - connection to the server. + """Generates an authenticator ID's for client session + connection to server. Returns: str: Returns auth_id diff --git a/settings/logging.py b/mod/logging.py similarity index 67% rename from settings/logging.py rename to mod/logging.py index 503452c2..426b830b 100644 --- a/settings/logging.py +++ b/mod/logging.py @@ -1,9 +1,17 @@ +import configparser + from loguru import logger import sys +# ************** Read "config.ini" ******************** +config = configparser.ConfigParser() +config.read('config.ini') +logging = config['LOGGING'] +# ************** END ********************************** + def add_logging(debug_status: int) -> None: - """Function enables logging depending on the start parameter uvicorn + """Enables logging depending on start parameter uvicorn Instead of print we use: # logger.debug('debug message') # @@ -18,41 +26,33 @@ def add_logging(debug_status: int) -> None: The information is also duplicated in the console Args: - debug_status (str, requires): ? + debug_status (int, requires): + CRITICAL-50; + ERROR-40; + WARNING-30; + SUCCES-25; + INFO-20; + DEBUG-10; + TRACE-5. - 50 - CRITICAL - 40 - ERROR - 30 - WARNING - 20 - INFO - 10 - DEBUG - Returns: None """ logger.remove() - debug_on = True if debug_status < 20 else False + DEBUG = True if debug_status < 20 else False - fmt = "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name: ^25} | \ - {function: ^15} | line:{line: >3} | {message}" - - if debug_on: + if DEBUG: # We connect the output to TTY, level DEBUG logger.add(sys.stdout, - format=fmt, + format=logging.get("debug"), level="DEBUG", enqueue=True, colorize=True) - logger_option = logger.opt(raw=True, - colors=True) - logger_option.info(f"{'-' * 40}\n" - f"{' Debug mode Included ':-^40}\n" - f"{'-' * 40}\n") - # Connect the output to a file, level DEBUG logger.add('log/debug.log', - format=fmt, + format=logging.get("debug"), level="DEBUG", enqueue=True, colorize=True, @@ -62,14 +62,14 @@ def add_logging(debug_status: int) -> None: else: # We connect the output to TTY, level INFO logger.add(sys.stdout, - format=fmt, + format=logging.get("info"), level="INFO", enqueue=True, colorize=True) # We connect the output to a file, level ERROR logger.add('log/error.log', - format=fmt, + format=logging.get("error"), level="ERROR", backtrace=True, diagnose=True, diff --git a/mod/models.py b/mod/models.py index 5a8f2486..0e2ad583 100644 --- a/mod/models.py +++ b/mod/models.py @@ -1,44 +1,12 @@ import sqlobject as orm -# Create table in database using ORM SQLobject -class Message(orm.SQLObject): - """The class generates a Message table containing information - about user messages. - - Args: - text (str, optional): - time (int, optional): - filePicture (byte, optional): - fileVideo (byte, optional): - fileAudio (byte, optional): - fileDocument (byte, optional): - emoji (str, optional): - editedTime (int, optional): - editedStatus (bool, optional): - - Returns: - None - """ - text = orm.StringCol(default=None) - time = orm.IntCol(default=None) - filePicture = orm.BLOBCol(default=None) - fileVideo = orm.BLOBCol(default=None) - fileAudio = orm.BLOBCol(default=None) - fileDocument = orm.BLOBCol(default=None) - emoji = orm.BLOBCol(default=None) - editedTime = orm.IntCol(default=None) - editedStatus = orm.BoolCol(default=False) - userConfig = orm.ForeignKey('UserConfig', refColumn="uuid") - flow = orm.ForeignKey('Flow', refColumn="flowId") - - class UserConfig(orm.SQLObject): - """The class generates a table containing data - about the user and his settings. + """Generates a table containing data + about user and his settings. Args: - uuid (int, required): + uuid (str, required): login (str, required): password (str, required): hash_password (str, optional) @@ -54,11 +22,9 @@ class UserConfig(orm.SQLObject): Returns: None """ - # added alternateID for added class method @byUUID - # which will return that object - uuid = orm.IntCol(alternateID=True, unique=True, notNone=True) - login = orm.StringCol() - password = orm.StringCol() + uuid = orm.StringCol(notNone=True, unique=True) + login = orm.StringCol(notNone=True) + password = orm.StringCol(notNone=True) hashPassword = orm.StringCol(default=None) username = orm.StringCol(default=None) isBot = orm.BoolCol(default=False) @@ -68,16 +34,17 @@ class UserConfig(orm.SQLObject): bio = orm.StringCol(default=None) salt = orm.BLOBCol(default=None) key = orm.BLOBCol(default=None) - # Connection to the Message table - message = orm.MultipleJoin('Message') + # Connection to Message and Flow table + messages = orm.MultipleJoin('Message') + flows = orm.RelatedJoin('Flow') class Flow(orm.SQLObject): - """The class generates a Flow table containing information + """Generates a Flow table containing information about threads and their types (chat, channel, group). Args: - flowId (int, required): + uuid (str, required): timeCreated (int, optional): flowType (str, optional): title (str, optional): @@ -86,28 +53,46 @@ class Flow(orm.SQLObject): Returns: None """ - flowId = orm.IntCol(alternateID=True, unique=True, notNone=True) + uuid = orm.StringCol(notNone=True, unique=True) timeCreated = orm.IntCol(default=None) flowType = orm.StringCol(default=None) title = orm.StringCol(default=None) info = orm.StringCol(default=None) - # Connection to the Message table - message = orm.MultipleJoin('Message') + owner = orm.StringCol(default=None) + # Connection to the Message and UserConfig table + messages = orm.MultipleJoin('Message') + users = orm.RelatedJoin('UserConfig') -class Errors(orm.SQLObject): - """The class generates an Errors table in which - all types of errors are pre-stored. +class Message(orm.SQLObject): + """Generates a Message table containing information + about user messages. Args: - status (str, optional): - code (int, optional): - detail (str, optional): + uuid (str, required): + text (str, optional): + time (int, optional): + filePicture (byte, optional): + fileVideo (byte, optional): + fileAudio (byte, optional): + fileDocument (byte, optional): + emoji (str, optional): + editedTime (int, optional): + editedStatus (bool, optional): Returns: None """ - # status and code is standart HTTP status code - status = orm.StringCol(default=None) - code = orm.IntCol(default=None) - detail = orm.StringCol(default=None) + uuid = orm.StringCol(notNone=True, unique=True) + text = orm.StringCol(default=None) + time = orm.IntCol(default=None) + filePicture = orm.BLOBCol(default=None) + fileVideo = orm.BLOBCol(default=None) + fileAudio = orm.BLOBCol(default=None) + fileDocument = orm.BLOBCol(default=None) + emoji = orm.BLOBCol(default=None) + editedTime = orm.IntCol(default=None) + editedStatus = orm.BoolCol(default=False) + # Connection to UserConfig and Flow table + user = orm.ForeignKey('UserConfig') + flow = orm.ForeignKey('Flow') diff --git a/server.py b/server.py index 5974a2f6..3a6f66a1 100644 --- a/server.py +++ b/server.py @@ -1,6 +1,7 @@ # ************** Standart module ********************* from datetime import datetime from json import JSONDecodeError +import configparser # ************** Standart module end ***************** @@ -15,41 +16,52 @@ # ************** Morelia module ********************** -from mod import config from mod import controller from mod import models -# ************** Morelia module end ******************** +# ************** Morelia module end ****************** -# ************** Logging beginning ********************* +# ************** Logging beginning ******************* from loguru import logger -from settings.logging import add_logging - -# ### unicorn logger off -# import logging -# logging.disable() - -# ### loguru logger on -add_logging(debug_status=config.DEBUG_LEVEL) -# ************** Logging end ************************** - -# Record server start time +from mod.logging import add_logging +# ************** Unicorn logger off ****************** +import logging +logging.disable() +# ************** Logging end ************************* + +# ************** Read "config.ini" ******************** +config = configparser.ConfigParser() +config.read('config.ini') +logging = config['LOGGING'] +database = config["DATABASE"] +directory = config["TEMPLATES"] +# ************** END ********************************** + +# loguru logger on +add_logging(logging.getint("level")) + +# Record server start time (UTC) server_started = datetime.now() # Connect to database -connection = orm.connectionForURI(config.LOCAL_POSTGRESQL) -orm.sqlhub.processConnection = connection +try: + connection = orm.connectionForURI(database.get("uri")) +except Exception as ERROR: + logger.exception(str(ERROR)) +finally: + orm.sqlhub.processConnection = connection # Server instance creation app = FastAPI() +logger.info("Start server") # Specifying where to load HTML page templates -templates = Jinja2Templates(directory=config.TEMPLATE_FOLDER) +templates = Jinja2Templates(directory.get("folder")) # Save clients session # TODO: Нужно подумать как их компактно хранить -clients = [] +CLIENTS = [] # Server home page @@ -65,7 +77,7 @@ def status_page(request: Request): stats = { 'Server time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'Server uptime': str(datetime.now()-server_started), - 'Users': len(clients), + 'Users': len(CLIENTS), 'Messages': dbquery.count() } return templates.TemplateResponse('status.html', @@ -76,32 +88,50 @@ def status_page(request: Request): # Chat websocket @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): + # Waiting for the client to connect via websockets await websocket.accept() - clients.append(websocket) + CLIENTS.append(websocket) logger.info("".join(("Clients information: ", "host: ", str(websocket.client.host), " port: ", str(websocket.client.port)))) - logger.debug(str(websocket.scope)) + logger.debug(f"Websocket scope: {str(websocket.scope)}") while True: try: + # Receive a request from the client as a JSON object data = await websocket.receive_json() - logger.debug(str(data)) + logger.success("Receive a request from client") + logger.debug(f"Request: {str(data)}") + # create a "client" object and pass the request body to + # it as a parameter. The "get_response" method generates + # a response in JSON-object format. client = controller.ProtocolMethods(data) - await websocket.send_bytes(client.get_response()) - except WebSocketDisconnect as error: - logger.info("".join(("Disconnection error: ", str(error)))) - clients.remove(websocket) + response = await websocket.send_bytes(client.get_response()) + logger.info("Response sent to client") + logger.debug(f"Result of processing: {response}") + # After disconnecting the client (by the decision of the client, + # the error) must interrupt the cycle otherwise the next clients + # will not be able to connect. + except WebSocketDisconnect as STATUS: + logger.debug(f"Disconnection status: {str(STATUS)}") + CLIENTS.remove(websocket) break - except (RuntimeError, JSONDecodeError) as error: - logger.info("".join(("Runtime or Decode error: ", str(error)))) - clients.remove(websocket) + except (RuntimeError, JSONDecodeError) as ERROR: + CODE = 1002 + logger.exception(f"Runtime or Decode error: {str(ERROR)}") + await websocket.close(CODE) + logger.info(f"Close with code: {CODE}") + CLIENTS.remove(websocket) break else: if websocket.client_state.value == 0: - await websocket.close(code=1000) - clients.remove(websocket) + CODE = 1000 + # "code=1000" - normal session termination + await websocket.close(CODE) + CLIENTS.remove(websocket) + logger.info(f"Close with code: {CODE}") if __name__ == "__main__": print("to start the server, write the following command in the console:") - print("uvicorn server:app --host 0.0.0.0 --port 8000 --reload --use-colors --http h11 --ws websockets &") + print("uvicorn server:app --host 0.0.0.0 --port 8000 --reload --use-colors \ + --http h11 --ws websockets &") diff --git a/tests/fixtures/api.json b/tests/fixtures/api.json index 538d2153..8c6de71a 100644 --- a/tests/fixtures/api.json +++ b/tests/fixtures/api.json @@ -3,25 +3,25 @@ "data": { "time": 1594492370, "flow": [{ - "id": 1254, + "uuid": "1254", "time": 1594492370, "type": "chat", "title": "Name Chat", "info": "Info about this chat" }, { - "id": 1254, + "uuid": "1254", "time": 1594492370, "type": "chat", "title": "Name Chat", "info": "Info about this chat" }], "message": [{ - "id": 1, + "uuid": 666, "text": "some text...", - "from_user_uuid": 1254, + "from_user": "1254", "time": 1594492370, - "from_flow_id": 123655455, + "from_flow": 123655455, "file_picture": "jkfikdkdsd", "file_video": "sdfsdfsdf", "file_audio": "fgfsdfsdfsdf", @@ -31,11 +31,11 @@ "edited_status": true }, { - "id": 1, + "uuid": "1", "text": "some text...", - "from_user_uuid": 1254, + "from_user": "1254", "time": 1594492370, - "from_flow_id": 123655455, + "from_flow": 123655455, "file_picture": "jkfikdkdsd", "file_video": "sdfsdfsdf", "file_audio": "fgfsdfsdfsdf", @@ -45,7 +45,7 @@ "edited_status": true }], "user": [{ - "uuid": 5855, + "uuid": "5855", "login": "username1", "password": "lksdjflksjfsd", "username": "Vasya", diff --git a/tests/test_api.py b/tests/test_api.py index eb890f97..f08342ee 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -5,7 +5,7 @@ # Add path to directory with code being checked # to variable 'PATH' to import modules from directory -# above the directory with the tests. +# above the directory with tests. BASE_PATH = os.path.abspath(os.path.dirname(__file__)) FIXTURES_PATH = os.path.join(BASE_PATH, 'fixtures') VALID_JSON = os.path.join(FIXTURES_PATH, 'api.json') diff --git a/tests/test_controller.py b/tests/test_controller.py index 18e38992..c2641f01 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -3,6 +3,8 @@ import os import sys import unittest +import configparser +from uuid import uuid4 import sqlobject as orm from loguru import logger @@ -19,6 +21,12 @@ from mod import lib # noqa from mod import models # noqa +# ************** Read "config.ini" ******************** +config = configparser.ConfigParser() +config.read('config.ini') +limit = config['SERVER_LIMIT'] +# ************** END ********************************** + connection = orm.connectionForURI("sqlite:/:memory:") orm.sqlhub.processConnection = connection @@ -27,14 +35,25 @@ if inspect.isclass(cls_obj)] -# JSON-object for test +# **************** Examples of requests ******************** +# +# +# variables for repeating fields in queries: +user_uuid = "123456" +user_auth_id = "auth_id" +user_password = "password" +user_login = "login" +flow_uuid = "07d949" +# +# +# requests GET_UPDATE = { "type": "get_update", "data": { "time": 111, "user": [{ - "uuid": 123456, - "auth_id": "auth_id" + "uuid": user_uuid, + "auth_id": user_auth_id }], "meta": None }, @@ -48,10 +67,11 @@ "type": "send_message", "data": { "flow": [{ - "id": 123 + "uuid": flow_uuid }], "message": [{ "text": "Hello!", + "client_id": 123, "file_picture": b"jkfikdkdsd", "file_video": b"sdfsdfsdf", "file_audio": b"fgfsdfsdfsdf", @@ -59,8 +79,8 @@ "emoji": b"sfdfsdfsdf" }], "user": [{ - "uuid": 123456, - "auth_id": "auth_id", + "uuid": user_uuid, + "auth_id": user_auth_id }], "meta": None }, @@ -75,11 +95,11 @@ "data": { "time": 2, "flow": [{ - "id": 123 - }], + "uuid": flow_uuid + }], "user": [{ - "uuid": 123456, - "auth_id": "auth_id" + "uuid": user_uuid, + "auth_id": user_auth_id }], "meta": None }, @@ -95,11 +115,13 @@ "flow": [{ "type": "group", "title": "title", - "info": "info" + "info": "info", + "owner": "123456", + "users": ["123456"] }], "user": [{ - "uuid": 123456, - "auth_id": "auth_id", + "uuid": user_uuid, + "auth_id": user_auth_id }], "meta": None }, @@ -113,8 +135,8 @@ "type": "all_flow", "data": { "user": [{ - "uuid": 123456, - "auth_id": "auth_id" + "uuid": user_uuid, + "auth_id": user_auth_id }], "meta": None }, @@ -128,8 +150,20 @@ "type": "user_info", "data": { "user": [{ - "uuid": 123456, - "auth_id": "auth_id" + "uuid": user_uuid, + "auth_id": user_auth_id + }, + { + "uuid": "123457" + }, + { + "uuid": "123458" + }, + { + "uuid": "123459" + }, + { + "uuid": "123460" }], "meta": None }, @@ -143,8 +177,8 @@ "type": "register_user", "data": { "user": [{ - "password": "password", - "login": "login", + "password": user_password, + "login": user_login, "email": "querty@querty.com", "username": "username" }], @@ -160,8 +194,8 @@ "type": "auth", "data": { "user": [{ - "password": "password", - "login": "login" + "password": user_password, + "login": user_login }], "meta": None }, @@ -175,10 +209,10 @@ "type": "delete_user", "data": { "user": [{ - "uuid": 123456, - "password": "password", - "login": "login", - "auth_id": "auth_id" + "uuid": user_uuid, + "password": user_password, + "login": user_login, + "auth_id": user_auth_id }], "meta": None }, @@ -191,12 +225,15 @@ DELETE_MESSAGE = { "type": "delete_message", "data": { + "flow": [{ + "uuid": flow_uuid + }], "message": [{ - "id": 1 + "uuid": "1122" }], "user": [{ - "uuid": 123456, - "auth_id": "auth_id" + "uuid": user_uuid, + "auth_id": user_auth_id }], "meta": None }, @@ -210,12 +247,12 @@ "type": "edited_message", "data": { "message": [{ - "id": 1, + "uuid": "1", "text": "New_Hello" }], "user": [{ - "uuid": 123456, - "auth_id": "auth_id" + "uuid": user_uuid, + "auth_id": user_auth_id }], "meta": None }, @@ -229,8 +266,8 @@ "type": "ping-pong", "data": { "user": [{ - "uuid": 123456, - "auth_id": "auth_id" + "uuid": user_uuid, + "auth_id": user_auth_id }], "meta": None }, @@ -244,8 +281,8 @@ "type": "wrong type", "data": { "user": [{ - "uuid": 123456, - "auth_id": "auth_id" + "uuid": user_uuid, + "auth_id": user_auth_id }], "meta": None }, @@ -256,12 +293,10 @@ } NON_VALID_ERRORS = { - # None valid blank dict - "type": {}, "data": { "user": [{ - "uuid": 123456, - "auth_id": "auth_id" + "uuid": user_uuid, + "auth_id": user_auth_id }], "meta": None }, @@ -271,7 +306,11 @@ "meta": None } -# end +ERRORS_ONLY_TYPE = { + "type": "send_message" + } + +# **************** End examples of requests ***************** class TestCheckAuthToken(unittest.TestCase): @@ -283,7 +322,7 @@ def setUp(self): for item in classes: class_ = getattr(models, item) class_.createTable(ifNotExists=True) - models.UserConfig(uuid=123456, + models.UserConfig(uuid="123456", login="login", password="password", authId="auth_id") @@ -331,7 +370,7 @@ def setUp(self): for item in classes: class_ = getattr(models, item) class_.createTable(ifNotExists=True) - models.UserConfig(uuid=123456, + models.UserConfig(uuid="123456", login="login", password="password", authId="auth_id") @@ -382,7 +421,7 @@ def test_user_created(self): self.assertEqual(result["errors"]["code"], 201) def test_user_already_exists(self): - models.UserConfig(uuid=123456, + models.UserConfig(uuid="123456", login="login", password="password") run_method = controller.ProtocolMethods(self.test) @@ -428,36 +467,60 @@ def setUp(self): for item in classes: class_ = getattr(models, item) class_.createTable(ifNotExists=True) - new_user1 = models.UserConfig(uuid=123456, + new_user1 = models.UserConfig(uuid="123456", login="login", password="password", authId="auth_id") - new_user2 = models.UserConfig(uuid=987654, + new_user2 = models.UserConfig(uuid="987654", login="login2", password="password2", authId="auth_id2") - new_flow1 = models.Flow(flowId=1, + new_user3 = models.UserConfig(uuid="666555", + login="login3", + password="password3", + authId="auth_id3") + new_flow1 = models.Flow(uuid="07d949", timeCreated=111, - flowType='chat', - title='title2', - info='info2') - new_flow2 = models.Flow(flowId=2, + flowType="chat", + title="title1", + info="info1", + owner="123456") + new_flow2 = models.Flow(uuid="07d950", timeCreated=222, - flowType='chat', - title='title2', - info='info2') - models.Message(text="Hello1", + flowType="group", + title="title2", + info="info2", + owner="987654") + new_flow1.addUserConfig(new_user1) + new_flow1.addUserConfig(new_user2) + new_flow2.addUserConfig(new_user2) + new_flow2.addUserConfig(new_user1) + new_flow2.addUserConfig(new_user3) + models.Message(uuid="111", + text="Hello1", time=111, - userConfig=new_user1, + user=new_user1, flow=new_flow1) - models.Message(text="Hello2", + models.Message(uuid="112", + text="Hello2", time=222, - userConfig=new_user2, + user=new_user2, + flow=new_flow1) + models.Message(uuid="113", + text="Heeeello1", + time=111, + user=new_user1, flow=new_flow2) - models.Message(text="Hello3", + models.Message(uuid="114", + text="Heeeello2", + time=222, + user=new_user2, + flow=new_flow2) + models.Message(uuid="115", + text="Heeeello3", time=333, - userConfig=new_user1, - flow=new_flow1) + user=new_user3, + flow=new_flow2) self.test = api.ValidJSON.parse_obj(GET_UPDATE) def tearDown(self): @@ -476,8 +539,20 @@ def test_update(self): def test_check_message_in_result(self): run_method = controller.ProtocolMethods(self.test) result = json.loads(run_method.get_response()) - self.assertEqual(result["data"]["message"][1]["text"], - "Hello2") + self.assertEqual(result["data"]["message"][1]["uuid"], + "112") + + def test_check_flow_in_result(self): + run_method = controller.ProtocolMethods(self.test) + result = json.loads(run_method.get_response()) + self.assertEqual(result["data"]["flow"][0]["owner"], + "123456") + + def test_check_user_in_result(self): + run_method = controller.ProtocolMethods(self.test) + result = json.loads(run_method.get_response()) + self.assertEqual(result["data"]["user"][2]["uuid"], + "666555") @unittest.skip("Не работает, пока не будет добавлен фильтр по времени") def test_no_new_data_in_database(self): @@ -496,13 +571,15 @@ def setUp(self): for item in classes: class_ = getattr(models, item) class_.createTable(ifNotExists=True) - models.UserConfig(uuid=123456, - login="login", - password="password", - authId="auth_id") - models.Flow(flowId=123, - timeCreated=111, - flowType="chat") + new_user = models.UserConfig(uuid="123456", + login="login", + password="password", + authId="auth_id") + new_flow = models.Flow(uuid="07d949", + timeCreated=111, + flowType="group", + owner="123456") + new_flow.addUserConfig(new_user) self.test = api.ValidJSON.parse_obj(SEND_MESSAGE) def tearDown(self): @@ -518,36 +595,135 @@ def test_send_message(self): result = json.loads(run_method.get_response()) self.assertEqual(result["errors"]["code"], 200) + def test_check_id_in_response(self): + run_method = controller.ProtocolMethods(self.test) + result = json.loads(run_method.get_response()) + dbquery = models.Message.selectBy().getOne() + self.assertEqual(result["data"]["message"][0]["uuid"], + dbquery.uuid) + + def test_check_client_id_in_response(self): + run_method = controller.ProtocolMethods(self.test) + result = json.loads(run_method.get_response()) + self.assertEqual(result["data"]["message"][0]["client_id"], + 123) + def test_wrong_flow(self): - self.test.data.flow[0].id = 666 + self.test.data.flow[0].uuid = "666666" run_method = controller.ProtocolMethods(self.test) result = json.loads(run_method.get_response()) self.assertEqual(result["errors"]["code"], 404) def test_write_text_in_database(self): - flow_id = self.test.data.flow[0].id controller.ProtocolMethods(self.test) - dbquery = models.Message.selectBy(flowID=flow_id).getOne() + dbquery = models.Message.selectBy().getOne() self.assertEqual(dbquery.text, self.test.data.message[0].text) def test_write_time_in_database(self): - flow_id = self.test.data.flow[0].id controller.ProtocolMethods(self.test) - dbquery = models.Message.selectBy(flowID=flow_id).getOne() + dbquery = models.Message.selectBy().getOne() self.assertIsInstance(dbquery.time, int) +class TestAllMessages(unittest.TestCase): + @classmethod + def setUpClass(cls): + logger.remove() + + def setUp(self): + for item in classes: + class_ = getattr(models, item) + class_.createTable(ifNotExists=True) + new_user = models.UserConfig(uuid="123456", + login="login", + password="password", + authId="auth_id") + new_user2 = models.UserConfig(uuid="654321", + login="login2", + password="password2", + authId="auth_id2") + new_flow = models.Flow(uuid="07d949", + flowType="chat", + owner="123456") + new_flow2 = models.Flow(uuid="07d950", + flowType="chat", + owner="654321") + new_flow.addUserConfig(new_user) + new_flow.addUserConfig(new_user2) + new_flow2.addUserConfig(new_user) + new_flow2.addUserConfig(new_user2) + for item in range(limit.getint("messages") + 10): + models.Message(uuid=str(uuid4().int), + text=f"Hello{item}", + time=item, + user=new_user, + flow=new_flow) + for item in range(limit.getint("messages") - 10): + models.Message(uuid=str(uuid4().int), + text=f"Kak Dela{item}", + time=item, + user=new_user2, + flow=new_flow2) + models.Message(uuid=str(uuid4().int), + text="Privet", + time=666, + user=new_user2, + flow=new_flow2) + self.test = api.ValidJSON.parse_obj(ALL_MESSAGES) + + def tearDown(self): + for item in classes: + class_ = getattr(models, item) + class_.dropTable(ifExists=True, + dropJoinTables=True, + cascade=True) + del self.test + + def test_all_message_more_limit(self): + run_method = controller.ProtocolMethods(self.test) + result = json.loads(run_method.get_response()) + self.assertEqual(result["errors"]["code"], 206) + + def test_all_message_less_limit(self): + self.test.data.flow[0].uuid = "07d950" + run_method = controller.ProtocolMethods(self.test) + result = json.loads(run_method.get_response()) + self.assertEqual(result["errors"]["code"], 200) + + def test_message_end_in_response(self): + run_method = controller.ProtocolMethods(self.test) + result = json.loads(run_method.get_response()) + self.assertEqual(result["data"]["flow"][0]["message_end"], 108) + + def test_check_message_in_database(self): + controller.ProtocolMethods(self.test) + dbquery = models.Message.selectBy(time=666).getOne() + self.assertEqual(dbquery.text, "Privet") + + def test_wrong_message_volume(self): + self.test.data.flow[0].message_end = 256 + run_method = controller.ProtocolMethods(self.test) + result = json.loads(run_method.get_response()) + self.assertEqual(result["errors"]["code"], 403) + + def test_wrong_flow_id(self): + self.test.data.flow[0].uuid = "666666" + run_method = controller.ProtocolMethods(self.test) + result = json.loads(run_method.get_response()) + self.assertEqual(result["errors"]["code"], 404) + + class TestAddFlow(unittest.TestCase): def setUp(self): for item in classes: class_ = getattr(models, item) class_.createTable(ifNotExists=True) - models.UserConfig(uuid=123456, + models.UserConfig(uuid="123456", login="login", password="password", authId="auth_id") - models.Flow(flowId=333) + models.Flow(uuid="07d949") logger.remove() self.test = api.ValidJSON.parse_obj(ADD_FLOW) @@ -571,31 +747,32 @@ def test_add_flow_channel(self): self.assertEqual(result["errors"]["code"], 200) def test_add_flow_bad_type(self): + error = "Wrong flow type" self.test.data.flow[0].type = "unknown" run_method = controller.ProtocolMethods(self.test) result = json.loads(run_method.get_response()) - self.assertEqual(result["errors"]["detail"], "Wrong flow type") + self.assertEqual(result["errors"]["detail"], error) def test_add_flow_chat_single_user(self): + error = "Must be two users only" self.test.data.flow[0].type = "chat" run_method = controller.ProtocolMethods(self.test) result = json.loads(run_method.get_response()) - self.assertEqual(result["errors"]["detail"], "Two users UUID must be specified for chat") + self.assertEqual(result["errors"]["detail"], error) - def test_add_flow_chat_double_user(self): + def test_add_flow_chat_more_users(self): self.test.data.flow[0].type = "chat" - self.test.data.user.append(api.User()) - self.test.data.user[1].uuid = 1234567 + self.test.data.flow[0].users.extend(["666555", "888999"]) run_method = controller.ProtocolMethods(self.test) result = json.loads(run_method.get_response()) - self.assertEqual(result["errors"]["code"], 200) + self.assertEqual(result["errors"]["code"], 400) def test_check_flow_in_database(self): run_method = controller.ProtocolMethods(self.test) result = json.loads(run_method.get_response()) dbquery = models.Flow.selectBy(title="title").getOne() - self.assertEqual(dbquery.flowId, - result["data"]["flow"][0]["id"]) + self.assertEqual(dbquery.uuid, + result["data"]["flow"][0]["uuid"]) class TestAllFlow(unittest.TestCase): @@ -607,7 +784,7 @@ def setUp(self): for item in classes: class_ = getattr(models, item) class_.createTable(ifNotExists=True) - models.UserConfig(uuid=123456, + models.UserConfig(uuid="123456", login="login", password="password", authId="auth_id") @@ -622,11 +799,12 @@ def tearDown(self): del self.test def test_all_flow(self): - models.Flow(flowId=1, + models.Flow(uuid="07d949", timeCreated=123456, - flowType="flow_type", + flowType="group", title="title", - info="info") + info="info", + owner="123456") run_method = controller.ProtocolMethods(self.test) result = json.loads(run_method.get_response()) self.assertEqual(result["data"]["flow"][0]["info"], "info") @@ -646,14 +824,16 @@ def setUp(self): for item in classes: class_ = getattr(models, item) class_.createTable(ifNotExists=True) - models.UserConfig(uuid=123456, - login="login", - password="password", - username="username", - isBot=False, - authId="auth_id", - email='email@email.com', - bio='bio') + for item in range(5): + uuid = str(123456 + item) + models.UserConfig(uuid=uuid, + login="login", + password="password", + username="username", + isBot=False, + authId="auth_id", + email="email@email.com", + bio="bio") self.test = api.ValidJSON.parse_obj(USER_INFO) def tearDown(self): @@ -674,6 +854,13 @@ def test_check_user_info(self): result = json.loads(run_method.get_response()) self.assertEqual(result["data"]["user"][0]["bio"], "bio") + def test_check_many_user_info(self): + users = [{'uuid': str(123456 + item)} for item in range(120)] + self.test.data.user.extend(users) + run_method = controller.ProtocolMethods(self.test) + result = json.loads(run_method.get_response()) + self.assertEqual(result["errors"]["code"], 403) + class TestAuthentification(unittest.TestCase): @classmethod @@ -687,7 +874,7 @@ def setUp(self): for item in classes: class_ = getattr(models, item) class_.createTable(ifNotExists=True) - models.UserConfig(uuid=123456, + models.UserConfig(uuid="123456", login="login", password="password", hashPassword=self.hash_password, @@ -717,7 +904,7 @@ def test_blank_database(self): self.assertEqual(result["errors"]["code"], 404) def test_two_element_in_database(self): - models.UserConfig(uuid=654321, + models.UserConfig(uuid="654321", login="login", password="password", salt=b"salt", @@ -746,7 +933,7 @@ def setUp(self): for item in classes: class_ = getattr(models, item) class_.createTable(ifNotExists=True) - models.UserConfig(uuid=123456, + models.UserConfig(uuid="123456", login="login", password="password", authId="auth_id") @@ -784,14 +971,20 @@ def setUp(self): for item in classes: class_ = getattr(models, item) class_.createTable(ifNotExists=True) - new_user = models.UserConfig(uuid=123456, + new_user = models.UserConfig(uuid="123456", login="login", password="password", authId="auth_id") - new_flow = models.Flow(flowId=123) - models.Message(text="Hello", + new_flow = models.Flow(uuid="07d949", + timeCreated=111, + flowType="group", + title="group", + owner="123456") + new_flow.addUserConfig(new_user) + models.Message(uuid="1122", + text="Hello", time=123456, - userConfig=new_user, + user=new_user, flow=new_flow) self.test = api.ValidJSON.parse_obj(DELETE_MESSAGE) logger.remove() @@ -814,8 +1007,13 @@ def test_check_delete_message_in_database(self): dbquery = models.Message.selectBy(text="Hello") self.assertEqual(dbquery.count(), 0) + def test_check_deleted_message_in_database(self): + controller.ProtocolMethods(self.test) + dbquery = models.Message.selectBy(text="Message deleted") + self.assertEqual(dbquery.count(), 1) + def test_wrong_message_id(self): - self.test.data.message[0].id = 2 + self.test.data.message[0].uuid = "2" run_method = controller.ProtocolMethods(self.test) result = json.loads(run_method.get_response()) self.assertEqual(result["errors"]["code"], 404) @@ -830,15 +1028,20 @@ def setUp(self): for item in classes: class_ = getattr(models, item) class_.createTable(ifNotExists=True) - new_user = models.UserConfig(uuid=123456, + new_user = models.UserConfig(uuid="123456", login="login", password="password", authId="auth_id") - new_flow = models.Flow(flowId=123) - models.Message(id=1, + new_flow = models.Flow(uuid="07d949", + timeCreated=112, + flowType="group", + title="group", + owner="123456") + new_flow.addUserConfig(new_user) + models.Message(uuid="1", text="Hello", time=123456, - userConfig=new_user, + user=new_user, flow=new_flow) self.test = api.ValidJSON.parse_obj(EDITED_MESSAGE) @@ -861,65 +1064,7 @@ def test_new_edited_message(self): self.assertEqual(dbquery.text, "New_Hello") def test_wrong_message_id(self): - self.test.data.message[0].id = 3 - run_method = controller.ProtocolMethods(self.test) - result = json.loads(run_method.get_response()) - self.assertEqual(result["errors"]["code"], 404) - - -class TestAllMessages(unittest.TestCase): - @classmethod - def setUpClass(cls): - logger.remove() - - def setUp(self): - for item in classes: - class_ = getattr(models, item) - class_.createTable(ifNotExists=True) - new_user = models.UserConfig(uuid=123456, - login="login", - password="password", - authId="auth_id") - new_user2 = models.UserConfig(uuid=654321, - login="login2", - password="password2", - authId="auth_id2") - new_flow = models.Flow(flowId=123) - new_flow2 = models.Flow(flowId=321) - models.Message(text="Hello", - time=1, - userConfig=new_user, - flow=new_flow) - models.Message(text="Privet", - time=2, - userConfig=new_user, - flow=new_flow) - models.Message(text="Hello2", - time=3, - userConfig=new_user2, - flow=new_flow2) - self.test = api.ValidJSON.parse_obj(ALL_MESSAGES) - - def tearDown(self): - for item in classes: - class_ = getattr(models, item) - class_.dropTable(ifExists=True, - dropJoinTables=True, - cascade=True) - del self.test - - def test_all_message(self): - run_method = controller.ProtocolMethods(self.test) - result = json.loads(run_method.get_response()) - self.assertEqual(result["errors"]["code"], 200) - - def test_check_message_in_database(self): - controller.ProtocolMethods(self.test) - dbquery = models.Message.selectBy(time=3).getOne() - self.assertEqual(dbquery.text, "Hello2") - - def test_wrong_flow_id(self): - self.test.data.flow[0].id = 555 + self.test.data.message[0].uuid = "3" run_method = controller.ProtocolMethods(self.test) result = json.loads(run_method.get_response()) self.assertEqual(result["errors"]["code"], 404) @@ -930,7 +1075,7 @@ def setUp(self): for item in classes: class_ = getattr(models, item) class_.createTable(ifNotExists=True) - models.UserConfig(uuid=123456, + models.UserConfig(uuid="123456", login="login", password="password", authId="auth_id") @@ -956,7 +1101,7 @@ def setUp(self): for item in classes: class_ = getattr(models, item) class_.createTable(ifNotExists=True) - models.UserConfig(uuid=123456, + models.UserConfig(uuid="123456", login="login", password="password", authId="auth_id") @@ -970,7 +1115,7 @@ def tearDown(self): cascade=True) del self.test - def test_wrong_type_method(self): + def test_wrong_type(self): self.test = api.ValidJSON.parse_obj(ERRORS) run_method = controller.ProtocolMethods(self.test) result = json.loads(run_method.get_response()) @@ -982,6 +1127,12 @@ def test_unsupported_media_type(self): result = json.loads(run_method.get_response()) self.assertEqual(result["errors"]["code"], 415) + def test_only_type_in_request(self): + self.test = json.dumps(ERRORS_ONLY_TYPE) + run_method = controller.ProtocolMethods(self.test) + result = json.loads(run_method.get_response()) + self.assertEqual(result["errors"]["code"], 415) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_lib.py b/tests/test_lib.py index eb6ec6a7..29ab8791 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -74,14 +74,6 @@ def test_wrong_hash_password_type(self): self.assertRaises(TypeError, generator.check_password) - def test_check_wrong_uuid_type(self): - self.uuid = '123123' - generator = lib.Hash(self.password, - self.uuid, - self.salt) - self.assertRaises(AttributeError, - generator.auth_id) - def test_check_password(self): self.new_generator = lib.Hash(self.password, self.uuid,