From 49c66e8cd3adce622456925b587924986cff798a Mon Sep 17 00:00:00 2001 From: ogolovatyi Date: Mon, 22 Apr 2019 17:01:38 -0700 Subject: [PATCH 01/13] Move unit tests to tests folder --- .travis.yml | 4 +- CONTRIBUTING.md | 49 ++++++++++++------- docs/server-config.md | 16 ++++++ .../tabpy_server/handlers/base_handler.py | 2 +- tests/runtests.py | 21 -------- .../server_tests/__init__.py | 0 .../server_tests/resources/expired.crt | 0 .../server_tests/resources/future.crt | 0 .../server_tests/resources/valid.crt | 0 .../server_tests/test_config.py | 0 .../server_tests/test_endpoint_handler.py | 0 .../server_tests/test_endpoints_handler.py | 0 .../test_evaluation_plane_handler.py | 0 .../server_tests/test_pwd_file.py | 0 .../server_tests/test_service_info_handler.py | 0 .../tools_tests/__init__.py | 0 .../tools_tests/test_client.py | 0 .../tools_tests/test_rest.py | 0 .../tools_tests/test_rest_object.py | 0 utils/user_management.py | 7 --- 20 files changed, 51 insertions(+), 48 deletions(-) delete mode 100755 tests/runtests.py rename {tabpy-server => tests}/server_tests/__init__.py (100%) rename {tabpy-server => tests}/server_tests/resources/expired.crt (100%) rename {tabpy-server => tests}/server_tests/resources/future.crt (100%) rename {tabpy-server => tests}/server_tests/resources/valid.crt (100%) rename {tabpy-server => tests}/server_tests/test_config.py (100%) rename {tabpy-server => tests}/server_tests/test_endpoint_handler.py (100%) rename {tabpy-server => tests}/server_tests/test_endpoints_handler.py (100%) rename {tabpy-server => tests}/server_tests/test_evaluation_plane_handler.py (100%) rename {tabpy-server => tests}/server_tests/test_pwd_file.py (100%) rename {tabpy-server => tests}/server_tests/test_service_info_handler.py (100%) rename {tabpy-tools => tests}/tools_tests/__init__.py (100%) rename {tabpy-tools => tests}/tools_tests/test_client.py (100%) rename {tabpy-tools => tests}/tools_tests/test_rest.py (100%) rename {tabpy-tools => tests}/tools_tests/test_rest_object.py (100%) diff --git a/.travis.yml b/.travis.yml index a2a65207..9e3fc050 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,8 @@ install: - pip install coveralls - npm install -g markdownlint-cli script: - - pytest tabpy-server/server_tests/ --cov=tabpy-server/tabpy_server - - pytest tabpy-tools/tools_tests/ --cov=tabpy-tools/tabpy_tools --cov-append + - pytest tests/server_tests/ --cov=tabpy-server/tabpy_server + - pytest tests/tools_tests/ --cov=tabpy-tools/tabpy_tools --cov-append - markdownlint . after_success: - coveralls diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4abb302e..bccca6c5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,8 +4,11 @@ - [Environment Setup](#environment-setup) - [Prerequisites](#prerequisites) -- [Windows Specific Steps](#windows-specific-steps) -- [Linux and Mac Specific Steps](#linux-and-mac-specific-steps) +- [Cloning TabPy Repository](#cloning-tabpy-repository) +- [Setting Up Environment](#setting-up-environment) +- [Unit Tests](#unit-tests) +- [Code Coverage](#code-coverage) +- [TabPy in Pythong Virtual Environment](#tabpy-in-pythong-virtual-environment) - [Documentation Updates](#documentation-updates) - [TabPy with Swagger](#tabpy-with-swagger) - [Code styling](#code-styling) @@ -30,10 +33,10 @@ be able to work on TabPy changes: - Create a new branch for your changes. - When changes are ready push them on github and create merge request. -## Windows Specific Steps +## Cloning TabPy Repository -1. Open a windows command prompt. -2. In the command prompt, navigate to the folder in which you would like to save +1. Open your OS shell. +2. Navigate to the folder in which you would like to save your local TabPy repository. 3. In the command prompt, enter the following commands: @@ -42,36 +45,48 @@ be able to work on TabPy changes: cd TabPy ``` -To start a local TabPy instance: +## Setting Up Environment + +Before making any code changes run environment setup script. For +Windows the next command from the repository root folder: ```sh -startup.cmd +utils\set_env.cmd ``` -To run the unit test suite: +and for Linux or Mac the next command from the repository root folder: ```sh -python tests\runtests.py +utils/set_env.sh ``` -Alternatively you can run unit tests to collect code coverage data. First -install `pytest`: +## Unit Tests + +TabPy has test suites for `tabpy-server` and `tabpy-tools` components. +To run the unit test use `pytest` which you may need to install first +(see [https://docs.pytest.org](https://docs.pytest.org) for details): ```sh -pip install pytest +pytest ``` -And then run `pytest` either for server or tools test, or even combined: +Check `pytest` documentation for how to run individual tests or set of tests. + +## Code Coverage + +You can run unit tests to collect code coverage data. To do so run `pytest` +either for server or tools test, or even combined: ```sh -pytest tabpy-server/server_tests/ --cov=tabpy-server/tabpy_server -pytest tabpy-tools/tools_tests/ --cov=tabpy-tools/tabpy_tools --cov-append +pytest tests/server_tests/ --cov=tabpy-server/tabpy_server +pytest tests/tools_tests/ --cov=tabpy-tools/tabpy_tools --cov-append ``` -## Linux and Mac Specific Steps +## TabPy in Pythong Virtual Environment If you have downloaded Tabpy and would like to manually install Tabpy Server -not using pip then follow the steps below [to run TabPy in Python virtual environment](docs/tabpy-virtualenv.md). +not using pip then follow the steps below +[to run TabPy in Python virtual environment](docs/tabpy-virtualenv.md). ## Documentation Updates diff --git a/docs/server-config.md b/docs/server-config.md index 6d1b9474..512bd62c 100755 --- a/docs/server-config.md +++ b/docs/server-config.md @@ -8,6 +8,7 @@ - [Authentication](#authentication) * [Enabling Authentication](#enabling-authentication) * [Password File](#password-file) + * [setting Up Environmnet](#setting-up-environmnet) * [Adding an Account](#adding-an-account) * [Updating an Account](#updating-an-account) * [Deleting an Account](#deleting-an-account) @@ -94,6 +95,21 @@ see how to use it. After making any changes to the password file, TabPy needs to be restarted. +### setting Up Environmnet + +Before making any code changes run environment setup script. For +Windows run the next command from the repository root folder: + +```sh +utils\set_env.cmd +``` + +and for Linux or Mac the next command from the repository root folder: + +```sh +utils/set_env.sh +``` + ### Adding an Account To add an account run `utils/user_management.py` utility with `add` diff --git a/tabpy-server/tabpy_server/handlers/base_handler.py b/tabpy-server/tabpy_server/handlers/base_handler.py index 7ea8bc4d..316bd5f5 100644 --- a/tabpy-server/tabpy_server/handlers/base_handler.py +++ b/tabpy-server/tabpy_server/handlers/base_handler.py @@ -316,7 +316,7 @@ def append_request_context(self, msg) -> str: if self.log_request_context: # log request details context = (f'{self.request.remote_ip} calls ' - '{self.request.method} {self.request.full_url()}') + f'{self.request.method} {self.request.full_url()}') if 'TabPy-Client' in self.request.headers: context += f', Client: {self.request.headers["TabPy-Client"]}' if 'TabPy-User' in self.request.headers: diff --git a/tests/runtests.py b/tests/runtests.py deleted file mode 100755 index c0da5329..00000000 --- a/tests/runtests.py +++ /dev/null @@ -1,21 +0,0 @@ -import os -import sys -import unittest - - -if __name__ == '__main__': - dirs = {'tabpy-tools', 'tabpy-server'} - - for dir_ in dirs: - sys.path.insert(0, os.path.join( - os.path.abspath(os.path.dirname(__file__)), dir_)) - - # Get all of the tests we need from the two project - suite_list = [] - for dir_ in dirs: - suite_list.append(unittest.TestLoader().discover(dir_)) - - suite = unittest.TestSuite(suite_list) - - runner = unittest.TextTestRunner() - runner.run(suite) diff --git a/tabpy-server/server_tests/__init__.py b/tests/server_tests/__init__.py similarity index 100% rename from tabpy-server/server_tests/__init__.py rename to tests/server_tests/__init__.py diff --git a/tabpy-server/server_tests/resources/expired.crt b/tests/server_tests/resources/expired.crt similarity index 100% rename from tabpy-server/server_tests/resources/expired.crt rename to tests/server_tests/resources/expired.crt diff --git a/tabpy-server/server_tests/resources/future.crt b/tests/server_tests/resources/future.crt similarity index 100% rename from tabpy-server/server_tests/resources/future.crt rename to tests/server_tests/resources/future.crt diff --git a/tabpy-server/server_tests/resources/valid.crt b/tests/server_tests/resources/valid.crt similarity index 100% rename from tabpy-server/server_tests/resources/valid.crt rename to tests/server_tests/resources/valid.crt diff --git a/tabpy-server/server_tests/test_config.py b/tests/server_tests/test_config.py similarity index 100% rename from tabpy-server/server_tests/test_config.py rename to tests/server_tests/test_config.py diff --git a/tabpy-server/server_tests/test_endpoint_handler.py b/tests/server_tests/test_endpoint_handler.py similarity index 100% rename from tabpy-server/server_tests/test_endpoint_handler.py rename to tests/server_tests/test_endpoint_handler.py diff --git a/tabpy-server/server_tests/test_endpoints_handler.py b/tests/server_tests/test_endpoints_handler.py similarity index 100% rename from tabpy-server/server_tests/test_endpoints_handler.py rename to tests/server_tests/test_endpoints_handler.py diff --git a/tabpy-server/server_tests/test_evaluation_plane_handler.py b/tests/server_tests/test_evaluation_plane_handler.py similarity index 100% rename from tabpy-server/server_tests/test_evaluation_plane_handler.py rename to tests/server_tests/test_evaluation_plane_handler.py diff --git a/tabpy-server/server_tests/test_pwd_file.py b/tests/server_tests/test_pwd_file.py similarity index 100% rename from tabpy-server/server_tests/test_pwd_file.py rename to tests/server_tests/test_pwd_file.py diff --git a/tabpy-server/server_tests/test_service_info_handler.py b/tests/server_tests/test_service_info_handler.py similarity index 100% rename from tabpy-server/server_tests/test_service_info_handler.py rename to tests/server_tests/test_service_info_handler.py diff --git a/tabpy-tools/tools_tests/__init__.py b/tests/tools_tests/__init__.py similarity index 100% rename from tabpy-tools/tools_tests/__init__.py rename to tests/tools_tests/__init__.py diff --git a/tabpy-tools/tools_tests/test_client.py b/tests/tools_tests/test_client.py similarity index 100% rename from tabpy-tools/tools_tests/test_client.py rename to tests/tools_tests/test_client.py diff --git a/tabpy-tools/tools_tests/test_rest.py b/tests/tools_tests/test_rest.py similarity index 100% rename from tabpy-tools/tools_tests/test_rest.py rename to tests/tools_tests/test_rest.py diff --git a/tabpy-tools/tools_tests/test_rest_object.py b/tests/tools_tests/test_rest_object.py similarity index 100% rename from tabpy-tools/tools_tests/test_rest_object.py rename to tests/tools_tests/test_rest_object.py diff --git a/utils/user_management.py b/utils/user_management.py index 5e3c28e0..52315d09 100755 --- a/utils/user_management.py +++ b/utils/user_management.py @@ -155,11 +155,4 @@ def main(): if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG, format="%(message)s") - # add tabpy-tools and tabpy-server folders to - # PYTHONPATH so code from there can be found when - # modules are imported - for dir_ in {'tabpy-tools', 'tabpy-server'}: - sys.path.insert(0, os.path.join( - os.path.abspath(os.path.dirname(__file__)), dir_)) - main() From db7d112dd6ddca36b295b5beda507a8ba0c9394a Mon Sep 17 00:00:00 2001 From: ogolovatyi Date: Mon, 22 Apr 2019 17:22:50 -0700 Subject: [PATCH 02/13] Update documentation for /info method --- .travis.yml | 3 +- CHANGELOG | 2 + CONTRIBUTING.md | 9 +- docs/server-rest.md | 95 +++++++++++++------ tests/{ => unit}/server_tests/__init__.py | 0 .../server_tests/resources/expired.crt | 0 .../server_tests/resources/future.crt | 0 .../server_tests/resources/valid.crt | 0 tests/{ => unit}/server_tests/test_config.py | 0 .../server_tests/test_endpoint_handler.py | 0 .../server_tests/test_endpoints_handler.py | 0 .../test_evaluation_plane_handler.py | 0 .../{ => unit}/server_tests/test_pwd_file.py | 0 .../server_tests/test_service_info_handler.py | 0 tests/{ => unit}/tools_tests/__init__.py | 0 tests/{ => unit}/tools_tests/test_client.py | 0 tests/{ => unit}/tools_tests/test_rest.py | 0 .../tools_tests/test_rest_object.py | 0 18 files changed, 76 insertions(+), 33 deletions(-) rename tests/{ => unit}/server_tests/__init__.py (100%) rename tests/{ => unit}/server_tests/resources/expired.crt (100%) rename tests/{ => unit}/server_tests/resources/future.crt (100%) rename tests/{ => unit}/server_tests/resources/valid.crt (100%) rename tests/{ => unit}/server_tests/test_config.py (100%) rename tests/{ => unit}/server_tests/test_endpoint_handler.py (100%) rename tests/{ => unit}/server_tests/test_endpoints_handler.py (100%) rename tests/{ => unit}/server_tests/test_evaluation_plane_handler.py (100%) rename tests/{ => unit}/server_tests/test_pwd_file.py (100%) rename tests/{ => unit}/server_tests/test_service_info_handler.py (100%) rename tests/{ => unit}/tools_tests/__init__.py (100%) rename tests/{ => unit}/tools_tests/test_client.py (100%) rename tests/{ => unit}/tools_tests/test_rest.py (100%) rename tests/{ => unit}/tools_tests/test_rest_object.py (100%) diff --git a/.travis.yml b/.travis.yml index 9e3fc050..4c26b228 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,7 @@ install: - pip install coveralls - npm install -g markdownlint-cli script: - - pytest tests/server_tests/ --cov=tabpy-server/tabpy_server - - pytest tests/tools_tests/ --cov=tabpy-tools/tabpy_tools --cov-append + - pytest tests --cov=tabpy-server/tabpy_server --cov=tabpy-tools/tabpy_tools --cov-append - markdownlint . after_success: - coveralls diff --git a/CHANGELOG b/CHANGELOG index db48fbe1..cdd9a4c0 100755 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Added request context logging as a feature controlled with TABPY_LOG_DETAILS configuration setting. +- Updated documentation for /info method +- Added integration tests ## v0.4 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bccca6c5..e670e3ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,19 +67,22 @@ To run the unit test use `pytest` which you may need to install first (see [https://docs.pytest.org](https://docs.pytest.org) for details): ```sh -pytest +pytest tests/unit ``` Check `pytest` documentation for how to run individual tests or set of tests. +## Integration Tests + +... + ## Code Coverage You can run unit tests to collect code coverage data. To do so run `pytest` either for server or tools test, or even combined: ```sh -pytest tests/server_tests/ --cov=tabpy-server/tabpy_server -pytest tests/tools_tests/ --cov=tabpy-tools/tabpy_tools --cov-append +pytest tests --cov=tabpy-server/tabpy_server --cov=tabpy-tools/tabpy_tools --cov-append ``` ## TabPy in Pythong Virtual Environment diff --git a/docs/server-rest.md b/docs/server-rest.md index 16b92e31..e4049ac6 100755 --- a/docs/server-rest.md +++ b/docs/server-rest.md @@ -3,20 +3,27 @@ The server process exposes several REST APIs to get status and to execute Python code and query deployed methods. + + - [http:get:: /info](#httpget-info) -- [http:get:: /status](#httpget-status) -- [http:get:: /endpoints](#httpget-endpoints) -- [http:get:: /endpoints/:endpoint](#httpget-endpointsendpoint) -- [http:post:: /evaluate](#httppost-evaluate) -- [http:post:: /query/:endpoint](#httppost-queryendpoint) +- [API v1](#api-v1) + * [http:get:: /status](#httpget-status) + * [http:get:: /endpoints](#httpget-endpoints) + * [http:get:: /endpoints/:endpoint](#httpget-endpointsendpoint) + * [http:post:: /evaluate](#httppost-evaluate) + * [http:post:: /query/:endpoint](#httppost-queryendpoint) + + ## http:get:: /info -Get static information about the server. +Get static information about the server. The method doesn't require any +authentication and returns supported API versions client can use together +with optional and required features. Example request: @@ -26,39 +33,71 @@ Host: localhost:9004 Accept: application/json ``` -Example response: - -```HTTP -HTTP/1.1 200 OK -Content-Type: application/json - -{"description": "", - "creation_time": "0", - "state_path": "/Users/username/my-server-state-folder", - "server_version": "dev", - "name": "my-server-name"} - +Example response (JSON response body for HTTP 200 status): + +```json +{ + "description": "", + "creation_time": "0", + "state_path": "e:\\dev\\TabPy\\tabpy-server\\tabpy_server", + "server_version": "0.4.1", + "name": "TabPy Server", + "versions": { + "v1": { + "features": { + "authentication": { + "required": true, + "methods": { + "basic-auth": {} + } + } + } + } + } +} ``` +In the response above there are some key properties: + - `description` is a string that is hardcoded in the `state.ini` file and - can be edited there. + can be edited there. - `creation_time` is the creation time in seconds since 1970-01-01, hardcoded - in the `state.ini` file, where it can be edited. + in the `state.ini` file, where it can be edited. - `state_path` is the state file path of the server (the value of the - TABPY_STATE_PATH at the time the server was started). + TABPY_STATE_PATH at the time the server was started). - `server_version` is the TabPy Server version tag. Clients can use this - information for compatibility checks. + information for compatibility checks. +- `name` is a string to describe TabPy server instance. Can be edited in + `state.ini` file. +- `version` is a collection of API versions supported by the server. Each + entry in the collection is an API version which has corresponding list + of properties. + +For each API version there is set of properties, e.g. for v1 in the example +abovefeatures are: + +- `authentiacation` - server has authentication feature enabled. +- `required` is an property of authentication feature and is required to be + used by a client. +- `methods` is a collection of supported authentication methods. In the + example above server only supports `basic-auth` which is for basic + access authentication (see + [TabPy Server Configuration Instructions](server-config.md) for how to + confire TabPy for authentication. + See [TabPy Configuration](#tabpy-configuration) section for more information on modifying the settings. -Using curl: +You can the method for a server with curl: ```bash curl -X GET http://localhost:9004/info ``` -## http:get:: /status +## API v1 + +### http:get:: /status Gets runtime status of deployed endpoints. If no endpoints are deployed in the server, the returned data is an empty JSON object. @@ -96,7 +135,7 @@ Using curl: curl -X GET http://localhost:9004/status ``` -## http:get:: /endpoints +### http:get:: /endpoints Gets a list of deployed endpoints and their static information. If no endpoints are deployed in the server, the returned data is an empty JSON object. @@ -142,7 +181,7 @@ Using curl: curl -X GET http://localhost:9004/endpoints ``` -## http:get:: /endpoints/:endpoint +### http:get:: /endpoints/:endpoint Gets the description of a specific deployed endpoint. The endpoint must first be deployed in the server (see the [TabPy Tools documentation](tabpy-tools.md)). @@ -172,7 +211,7 @@ Using curl: curl -X GET http://localhost:9004/endpoints/add ``` -## http:post:: /evaluate +### http:post:: /evaluate Executes a block of Python code, replacing named parameters with their provided values. @@ -243,7 +282,7 @@ curl -X POST http://localhost:9004/evaluate \ "script": "return tabpy.query(\"add\", x=_arg1, y=_arg2)[\"response\"]"}' ``` -## http:post:: /query/:endpoint +### http:post:: /query/:endpoint Executes a function at the specified endpoint. The function must first be deployed (see the [TabPy Tools documentation](tabpy-tools.md)). diff --git a/tests/server_tests/__init__.py b/tests/unit/server_tests/__init__.py similarity index 100% rename from tests/server_tests/__init__.py rename to tests/unit/server_tests/__init__.py diff --git a/tests/server_tests/resources/expired.crt b/tests/unit/server_tests/resources/expired.crt similarity index 100% rename from tests/server_tests/resources/expired.crt rename to tests/unit/server_tests/resources/expired.crt diff --git a/tests/server_tests/resources/future.crt b/tests/unit/server_tests/resources/future.crt similarity index 100% rename from tests/server_tests/resources/future.crt rename to tests/unit/server_tests/resources/future.crt diff --git a/tests/server_tests/resources/valid.crt b/tests/unit/server_tests/resources/valid.crt similarity index 100% rename from tests/server_tests/resources/valid.crt rename to tests/unit/server_tests/resources/valid.crt diff --git a/tests/server_tests/test_config.py b/tests/unit/server_tests/test_config.py similarity index 100% rename from tests/server_tests/test_config.py rename to tests/unit/server_tests/test_config.py diff --git a/tests/server_tests/test_endpoint_handler.py b/tests/unit/server_tests/test_endpoint_handler.py similarity index 100% rename from tests/server_tests/test_endpoint_handler.py rename to tests/unit/server_tests/test_endpoint_handler.py diff --git a/tests/server_tests/test_endpoints_handler.py b/tests/unit/server_tests/test_endpoints_handler.py similarity index 100% rename from tests/server_tests/test_endpoints_handler.py rename to tests/unit/server_tests/test_endpoints_handler.py diff --git a/tests/server_tests/test_evaluation_plane_handler.py b/tests/unit/server_tests/test_evaluation_plane_handler.py similarity index 100% rename from tests/server_tests/test_evaluation_plane_handler.py rename to tests/unit/server_tests/test_evaluation_plane_handler.py diff --git a/tests/server_tests/test_pwd_file.py b/tests/unit/server_tests/test_pwd_file.py similarity index 100% rename from tests/server_tests/test_pwd_file.py rename to tests/unit/server_tests/test_pwd_file.py diff --git a/tests/server_tests/test_service_info_handler.py b/tests/unit/server_tests/test_service_info_handler.py similarity index 100% rename from tests/server_tests/test_service_info_handler.py rename to tests/unit/server_tests/test_service_info_handler.py diff --git a/tests/tools_tests/__init__.py b/tests/unit/tools_tests/__init__.py similarity index 100% rename from tests/tools_tests/__init__.py rename to tests/unit/tools_tests/__init__.py diff --git a/tests/tools_tests/test_client.py b/tests/unit/tools_tests/test_client.py similarity index 100% rename from tests/tools_tests/test_client.py rename to tests/unit/tools_tests/test_client.py diff --git a/tests/tools_tests/test_rest.py b/tests/unit/tools_tests/test_rest.py similarity index 100% rename from tests/tools_tests/test_rest.py rename to tests/unit/tools_tests/test_rest.py diff --git a/tests/tools_tests/test_rest_object.py b/tests/unit/tools_tests/test_rest_object.py similarity index 100% rename from tests/tools_tests/test_rest_object.py rename to tests/unit/tools_tests/test_rest_object.py From 231c7662964667d5d986ec2e9604bdeaa2c4f49b Mon Sep 17 00:00:00 2001 From: ogolovatyi Date: Mon, 22 Apr 2019 17:23:27 -0700 Subject: [PATCH 03/13] Add set_env scripts --- utils/set_env.cmd | 1 + utils/set_env.sh | 1 + 2 files changed, 2 insertions(+) create mode 100755 utils/set_env.cmd create mode 100755 utils/set_env.sh diff --git a/utils/set_env.cmd b/utils/set_env.cmd new file mode 100755 index 00000000..6a7033f0 --- /dev/null +++ b/utils/set_env.cmd @@ -0,0 +1 @@ +set PYTHONPATH=%PYTHONPATH%;./tabpy-server;./tabpy-tools \ No newline at end of file diff --git a/utils/set_env.sh b/utils/set_env.sh new file mode 100755 index 00000000..4f683fcf --- /dev/null +++ b/utils/set_env.sh @@ -0,0 +1 @@ +export PYTHONPATH=./tabpy-server:./tabpy-tools:$PYTHONPATH \ No newline at end of file From 31eb53eb62de53105c503ddd39b32c413a8aea69 Mon Sep 17 00:00:00 2001 From: ogolovatyi Date: Tue, 23 Apr 2019 12:58:00 -0700 Subject: [PATCH 04/13] Updated API documentation for /info and authentication --- docs/api-v1.md | 276 ++++++++++++++++++++++++++++++++++ docs/server-rest.md | 350 +++++++++----------------------------------- 2 files changed, 349 insertions(+), 277 deletions(-) create mode 100755 docs/api-v1.md diff --git a/docs/api-v1.md b/docs/api-v1.md new file mode 100755 index 00000000..43c1c33c --- /dev/null +++ b/docs/api-v1.md @@ -0,0 +1,276 @@ +# TabPy API v1 + + + + + +- [Authentication](#authentication) + * [http:get:: /status](#httpget-status) + * [http:get:: /endpoints](#httpget-endpoints) + * [http:get:: /endpoints/:endpoint](#httpget-endpointsendpoint) + * [http:post:: /evaluate](#httppost-evaluate) + * [http:post:: /query/:endpoint](#httppost-queryendpoint) + + + + + +## Authentication + +When authentication feature is enabled for v1 API [`/info` call](server-rest.md#get-info) +response contains authentication feature parameters, e.g.: + + ```json + { + "description": "", + "creation_time": "0", + "state_path": "e:\\dev\\TabPy\\tabpy-server\\tabpy_server", + "server_version": "0.4.1", + "name": "TabPy Server", + "versions": { + "v1": { + "features": { + "authentication": { + "required": true, + "methods": { + "basic-auth": {} + } + } + } + } + } + } + ``` + +v1 authentication specific features (see the example above): + +Property | Description +--- | --- +`required` | Authentication is never optional for client to use if it is mentioned in features list. +`methods` | List of supported authentication methods with their properties. +`methods.basic-auth` | TabPy requires to use basic access authenticatio, see [TabPy Server Configuration Instructions](server-config.md#authentication) for how to configure authentication. + +### http:get:: /status + +Gets runtime status of deployed endpoints. If no endpoints are deployed in +the server, the returned data is an empty JSON object. + +Example request: + +```HTTP +GET /status HTTP/1.1 +Host: localhost:9004 +Accept: application/json +``` + +Example response: + +```HTTP +HTTP/1.1 200 OK +Content-Type: application/json + +{"clustering": { + "status": "LoadSuccessful", + "last_error": null, + "version": 1, + "type": "model"}, + "add": { + "status": "LoadSuccessful", + "last_error": null, + "version": 1, + "type": "model"} +} +``` + +Using curl: + +```bash +curl -X GET http://localhost:9004/status +``` + +### http:get:: /endpoints + +Gets a list of deployed endpoints and their static information. If no +endpoints are deployed in the server, the returned data is an empty JSON object. + +Example request: + +```HTTP +GET /endpoints HTTP/1.1 +Host: localhost:9004 +Accept: application/json +``` + +Example response: + +```HTTP +HTTP/1.1 200 OK +Content-Type: application/json + +{"clustering": + {"description": "", + "docstring": "-- no docstring found in query function --", + "creation_time": 1469511182, + "version": 1, + "dependencies": [], + "last_modified_time": 1469511182, + "type": "model", + "target": null}, +"add": { + "description": "", + "docstring": "-- no docstring found in query function --", + "creation_time": 1469505967, + "version": 1, + "dependencies": [], + "last_modified_time": 1469505967, + "type": "model", + "target": null} +} +``` + +Using curl: + +```bash +curl -X GET http://localhost:9004/endpoints +``` + +### http:get:: /endpoints/:endpoint + +Gets the description of a specific deployed endpoint. The endpoint must first +be deployed in the server (see the [TabPy Tools documentation](tabpy-tools.md)). + +Example request: + +```HTTP +GET /endpoints/add HTTP/1.1 +Host: localhost:9004 +Accept: application/json +``` + +Example response: + +```HTTP +HTTP/1.1 200 OK +Content-Type: application/json + +{"description": "", "docstring": "-- no docstring found in query function --", + "creation_time": 1469505967, "version": 1, "dependencies": [], + "last_modified_time": 1469505967, "type": "model", "target": null} +``` + +Using curl: + +```bash +curl -X GET http://localhost:9004/endpoints/add +``` + +### http:post:: /evaluate + +Executes a block of Python code, replacing named parameters with their provided +values. + +The expected POST body is a JSON dictionary with two elements: + +- A key `data` with a value that contains the parameter values passed to the + code. These values are key-value pairs, following a specific convention for + key names (`_arg1`, `_arg2`, etc.). +- A key `script` with a value that contains the Python code (one or more lines). + Any references to the parameter names will be replaced by their values + according to `data`. + +Example request: + +```HTTP +POST /evaluate HTTP/1.1 +Host: localhost:9004 +Accept: application/json + +{"data": {"_arg1": 1, "_arg2": 2}, "script": "return _arg1+_arg2"} +``` + +Example response: + +```HTTP +HTTP/1.1 200 OK +Content-Type: application/json + +3 +``` + +Using curl: + +```bash +curl -X POST http://localhost:9004/evaluate \ +-d '{"data": {"_arg1": 1, "_arg2": 2}, "script": "return _arg1 + _arg2"}' +``` + +It is possible to call a deployed function from within the code block, through +the predefined function `tabpy.query`. This function works like the client +library's `query` method, and returns the corresponding data structure. The +function must first be deployed as an endpoint in the server (for more details +see the [TabPy Tools documentation](tabpy-tools.md)). + +The following example calls the endpoint `clustering` as it was deployed in the +section [deploy-function](tabpy-tools.md#deploying-a-function): + +```HTTP +POST /evaluate HTTP/1.1 +Host: example.com +Accept: application/json + +{ "data": + { "_arg1": [6.35, 6.40, 6.65, 8.60, 8.90, 9.00, 9.10], + "_arg2": [1.95, 1.95, 2.05, 3.05, 3.05, 3.10, 3.15] + }, + "script": "return tabpy.query('clustering', x=_arg1, y=_arg2)"} +``` + +The next example shows how to call `evaluate` from a terminal using curl; this +code queries the method `add` that was deployed in the section +[deploy-function](tabpy-tools.md#deploying-a-function): + +```bash +curl -X POST http://localhost:9004/evaluate \ +-d '{"data": {"_arg1":1, "_arg2":2}, + "script": "return tabpy.query(\"add\", x=_arg1, y=_arg2)[\"response\"]"}' +``` + +### http:post:: /query/:endpoint + +Executes a function at the specified endpoint. The function must first be +deployed (see the [TabPy Tools documentation](tabpy-tools.md)). + +This interface expects a JSON body with a `data` key, specifying the values +for the function, according to its original definition. In the example below, +the function `clustering` was defined with a signature of two parameters `x` +and `y`, expecting arrays of numbers. + +Example request: + +```HTTP +POST /query/clustering HTTP/1.1 +Host: localhost:9004 +Accept: application/json + +{"data": { + "x": [6.35, 6.40, 6.65, 8.60, 8.90, 9.00, 9.10], + "y": [1.95, 1.95, 2.05, 3.05, 3.05, 3.10, 3.15]}} +``` + +Example response: + +```HTTP +HTTP/1.1 200 OK +Content-Type: application/json + +{"model": "clustering", "version": 1, "response": [0, 0, 0, 1, 1, 1, 1], + "uuid": "46d3df0e-acca-4560-88f1-67c5aedeb1c4"} +``` + +Using curl: + +```bash +curl -X GET http://localhost:9004/query/clustering -d \ +'{"data": {"x": [6.35, 6.40, 6.65, 8.60, 8.90, 9.00, 9.10], + "y": [1.95, 1.95, 2.05, 3.05, 3.05, 3.10, 3.15]}}' +``` diff --git a/docs/server-rest.md b/docs/server-rest.md index e4049ac6..8b02ff6c 100755 --- a/docs/server-rest.md +++ b/docs/server-rest.md @@ -7,71 +7,87 @@ Python code and query deployed methods. -- [http:get:: /info](#httpget-info) -- [API v1](#api-v1) - * [http:get:: /status](#httpget-status) - * [http:get:: /endpoints](#httpget-endpoints) - * [http:get:: /endpoints/:endpoint](#httpget-endpointsendpoint) - * [http:post:: /evaluate](#httppost-evaluate) - * [http:post:: /query/:endpoint](#httppost-queryendpoint) +- [GET /info](#get-info) + * [URL](#url) + * [Method](#method) + * [URL parameters](#url-parameters) + * [Data Parameters](#data-parameters) + * [Response](#response) +- [API versions](#api-versions) -## http:get:: /info +## GET /info Get static information about the server. The method doesn't require any authentication and returns supported API versions client can use together with optional and required features. -Example request: +### URL ```HTTP -GET /info HTTP/1.1 -Host: localhost:9004 -Accept: application/json +/info ``` -Example response (JSON response body for HTTP 200 status): +### Method -```json -{ - "description": "", - "creation_time": "0", - "state_path": "e:\\dev\\TabPy\\tabpy-server\\tabpy_server", - "server_version": "0.4.1", - "name": "TabPy Server", - "versions": { - "v1": { - "features": { - "authentication": { - "required": true, - "methods": { - "basic-auth": {} - } - } - } - } - } -} -``` - -In the response above there are some key properties: - -- `description` is a string that is hardcoded in the `state.ini` file and - can be edited there. -- `creation_time` is the creation time in seconds since 1970-01-01, hardcoded - in the `state.ini` file, where it can be edited. -- `state_path` is the state file path of the server (the value of the - TABPY_STATE_PATH at the time the server was started). -- `server_version` is the TabPy Server version tag. Clients can use this - information for compatibility checks. -- `name` is a string to describe TabPy server instance. Can be edited in - `state.ini` file. -- `version` is a collection of API versions supported by the server. Each - entry in the collection is an API version which has corresponding list - of properties. +```HTTP +GET +``` + +### URL parameters + +None. + +### Data Parameters + +None. + +### Response + +For successful call: + +- Status: 200 +- Content: + + ```json + { + "description": "", + "creation_time": "0", + "state_path": "e:\\dev\\TabPy\\tabpy-server\\tabpy_server", + "server_version": "0.4.1", + "name": "TabPy Server", + "versions": { + "v1": { + "features": { + "authentication": { + "required": true, + "methods": { + "basic-auth": {} + } + } + } + } + } + } + ``` + +Response fields: + +Property | Description +--- | --- +`description` | String that is hardcoded in the `state.ini` file and can be edited there. +`creation_time` | creation time in seconds since 1970-01-01, hardcoded in the `state.ini` file, where it can be edited. +`state_path` | state file path of the server (the value of the TABPY_STATE_PATH at the time the server was started). +`server_version` | TabPy Server version tag. Clients can use this information for compatibility checks. +`name` | TabPy server instance name. Can be edited in `state.ini` file. +`version` | Collection of API versions supported by the server. Each entry in the collection is an API version which has corresponding list of properties. +`version.`*``* | Set of properties for an API version. +`version.`*`.features`* | Set of an API available features. +`version.`*`.features.`* | Set of a features properties. For specific details for property meaning of a feature check documentation for specific API version. +`version.`*`.features..required`* | If true the feature is required to be used by client. For each API version there is set of properties, e.g. for v1 in the example abovefeatures are: @@ -81,243 +97,23 @@ abovefeatures are: used by a client. - `methods` is a collection of supported authentication methods. In the example above server only supports `basic-auth` which is for basic - access authentication (see + access authentication (see [TabPy Server Configuration Instructions](server-config.md) for how to confire TabPy for authentication. - See [TabPy Configuration](#tabpy-configuration) section for more information on modifying the settings. -You can the method for a server with curl: - -```bash -curl -X GET http://localhost:9004/info -``` - -## API v1 - -### http:get:: /status - -Gets runtime status of deployed endpoints. If no endpoints are deployed in -the server, the returned data is an empty JSON object. - -Example request: - -```HTTP -GET /status HTTP/1.1 -Host: localhost:9004 -Accept: application/json -``` - -Example response: - -```HTTP -HTTP/1.1 200 OK -Content-Type: application/json - -{"clustering": { - "status": "LoadSuccessful", - "last_error": null, - "version": 1, - "type": "model"}, - "add": { - "status": "LoadSuccessful", - "last_error": null, - "version": 1, - "type": "model"} -} -``` - -Using curl: - -```bash -curl -X GET http://localhost:9004/status -``` - -### http:get:: /endpoints - -Gets a list of deployed endpoints and their static information. If no -endpoints are deployed in the server, the returned data is an empty JSON object. - -Example request: - -```HTTP -GET /endpoints HTTP/1.1 -Host: localhost:9004 -Accept: application/json -``` - -Example response: - -```HTTP -HTTP/1.1 200 OK -Content-Type: application/json - -{"clustering": - {"description": "", - "docstring": "-- no docstring found in query function --", - "creation_time": 1469511182, - "version": 1, - "dependencies": [], - "last_modified_time": 1469511182, - "type": "model", - "target": null}, -"add": { - "description": "", - "docstring": "-- no docstring found in query function --", - "creation_time": 1469505967, - "version": 1, - "dependencies": [], - "last_modified_time": 1469505967, - "type": "model", - "target": null} -} -``` - -Using curl: - -```bash -curl -X GET http://localhost:9004/endpoints -``` - -### http:get:: /endpoints/:endpoint - -Gets the description of a specific deployed endpoint. The endpoint must first -be deployed in the server (see the [TabPy Tools documentation](tabpy-tools.md)). - -Example request: - -```HTTP -GET /endpoints/add HTTP/1.1 -Host: localhost:9004 -Accept: application/json -``` - -Example response: +- **Examples** -```HTTP -HTTP/1.1 200 OK -Content-Type: application/json - -{"description": "", "docstring": "-- no docstring found in query function --", - "creation_time": 1469505967, "version": 1, "dependencies": [], - "last_modified_time": 1469505967, "type": "model", "target": null} -``` - -Using curl: - -```bash -curl -X GET http://localhost:9004/endpoints/add -``` - -### http:post:: /evaluate - -Executes a block of Python code, replacing named parameters with their provided -values. - -The expected POST body is a JSON dictionary with two elements: - -- A key `data` with a value that contains the parameter values passed to the - code. These values are key-value pairs, following a specific convention for - key names (`_arg1`, `_arg2`, etc.). -- A key `script` with a value that contains the Python code (one or more lines). - Any references to the parameter names will be replaced by their values - according to `data`. - -Example request: - -```HTTP -POST /evaluate HTTP/1.1 -Host: localhost:9004 -Accept: application/json - -{"data": {"_arg1": 1, "_arg2": 2}, "script": "return _arg1+_arg2"} -``` - -Example response: - -```HTTP -HTTP/1.1 200 OK -Content-Type: application/json - -3 -``` - -Using curl: +Calling the method with curl: ```bash -curl -X POST http://localhost:9004/evaluate \ --d '{"data": {"_arg1": 1, "_arg2": 2}, "script": "return _arg1 + _arg2"}' -``` - -It is possible to call a deployed function from within the code block, through -the predefined function `tabpy.query`. This function works like the client -library's `query` method, and returns the corresponding data structure. The -function must first be deployed as an endpoint in the server (for more details -see the [TabPy Tools documentation](tabpy-tools.md)). - -The following example calls the endpoint `clustering` as it was deployed in the -section [deploy-function](tabpy-tools.md#deploying-a-function): - -```HTTP -POST /evaluate HTTP/1.1 -Host: example.com -Accept: application/json - -{ "data": - { "_arg1": [6.35, 6.40, 6.65, 8.60, 8.90, 9.00, 9.10], - "_arg2": [1.95, 1.95, 2.05, 3.05, 3.05, 3.10, 3.15] - }, - "script": "return tabpy.query('clustering', x=_arg1, y=_arg2)"} -``` - -The next example shows how to call `evaluate` from a terminal using curl; this -code queries the method `add` that was deployed in the section -[deploy-function](tabpy-tools.md#deploying-a-function): - -```bash -curl -X POST http://localhost:9004/evaluate \ --d '{"data": {"_arg1":1, "_arg2":2}, - "script": "return tabpy.query(\"add\", x=_arg1, y=_arg2)[\"response\"]"}' -``` - -### http:post:: /query/:endpoint - -Executes a function at the specified endpoint. The function must first be -deployed (see the [TabPy Tools documentation](tabpy-tools.md)). - -This interface expects a JSON body with a `data` key, specifying the values -for the function, according to its original definition. In the example below, -the function `clustering` was defined with a signature of two parameters `x` -and `y`, expecting arrays of numbers. - -Example request: - -```HTTP -POST /query/clustering HTTP/1.1 -Host: localhost:9004 -Accept: application/json - -{"data": { - "x": [6.35, 6.40, 6.65, 8.60, 8.90, 9.00, 9.10], - "y": [1.95, 1.95, 2.05, 3.05, 3.05, 3.10, 3.15]}} +curl -X GET http://localhost:9004/info ``` -Example response: +## API versions -```HTTP -HTTP/1.1 200 OK -Content-Type: application/json +TabPy supports the following API versions: -{"model": "clustering", "version": 1, "response": [0, 0, 0, 1, 1, 1, 1], - "uuid": "46d3df0e-acca-4560-88f1-67c5aedeb1c4"} -``` - -Using curl: - -```bash -curl -X GET http://localhost:9004/query/clustering -d \ -'{"data": {"x": [6.35, 6.40, 6.65, 8.60, 8.90, 9.00, 9.10], - "y": [1.95, 1.95, 2.05, 3.05, 3.05, 3.10, 3.15]}}' -``` +- v1 - see details at [api-v1.md](api-v1.md). From af03e4c11a71bda9e5f6d379c80af56e68e5c8b9 Mon Sep 17 00:00:00 2001 From: ogolovatyi Date: Tue, 23 Apr 2019 16:35:00 -0700 Subject: [PATCH 05/13] Add auth tests --- .vscode/launch.json | 16 ++ .vscode/settings.json | 9 +- docs/api-v1.md | 4 + docs/server-config.md | 2 + docs/server-rest.md | 13 +- startup.cmd | 2 +- tests/integration/resources/pwdfile.txt | 1 + tests/integration/test_auth.py | 141 ++++++++++++++++++ .../server_tests/test_endpoint_handler.py | 2 - tests/unit/server_tests/test_pwd_file.py | 10 +- 10 files changed, 179 insertions(+), 21 deletions(-) create mode 100755 .vscode/launch.json create mode 100755 tests/integration/resources/pwdfile.txt create mode 100755 tests/integration/test_auth.py diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100755 index 00000000..a5ebf5c5 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: General", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "externalTerminal", + "env": {"${PYTHONTPATH}": "${PYTHONPATH};${workspaceRoot}/tabpy-server;${workspaceRoot}/tabpy-tools"} + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 917bf975..569c1d56 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,12 @@ }, "python.linting.pylintEnabled": false, "python.linting.flake8Enabled": true, - "python.linting.enabled": true + "python.linting.enabled": true, + "python.unitTest.autoTestDiscoverOnSaveEnabled": true, + "python.unitTest.pyTestArgs": [ + "tests" + ], + "python.unitTest.unittestEnabled": false, + "python.unitTest.nosetestsEnabled": false, + "python.unitTest.pyTestEnabled": true } \ No newline at end of file diff --git a/docs/api-v1.md b/docs/api-v1.md index 43c1c33c..c2c963a0 100755 --- a/docs/api-v1.md +++ b/docs/api-v1.md @@ -44,12 +44,16 @@ response contains authentication feature parameters, e.g.: v1 authentication specific features (see the example above): + + Property | Description --- | --- `required` | Authentication is never optional for client to use if it is mentioned in features list. `methods` | List of supported authentication methods with their properties. `methods.basic-auth` | TabPy requires to use basic access authenticatio, see [TabPy Server Configuration Instructions](server-config.md#authentication) for how to configure authentication. + + ### http:get:: /status Gets runtime status of deployed endpoints. If no endpoints are deployed in diff --git a/docs/server-config.md b/docs/server-config.md index 512bd62c..a1042e97 100755 --- a/docs/server-config.md +++ b/docs/server-config.md @@ -161,6 +161,7 @@ URL, client infomation (Tableau Desktop\Server), Tableau user name (for Tableau and TabPy user name as shown in the example below: + ``` 2019-04-17,15:20:37 [INFO] (evaluation_plane_handler.py:evaluation_plane_handler:86): ::1 calls POST http://localhost:9004/evaluate, @@ -173,4 +174,5 @@ function to evaluate=def _user_script(tabpy, _arg1, _arg2): res.append(_arg1[i] * _arg2[i]) return res ``` + diff --git a/docs/server-rest.md b/docs/server-rest.md index 8b02ff6c..de2490aa 100755 --- a/docs/server-rest.md +++ b/docs/server-rest.md @@ -76,6 +76,8 @@ For successful call: Response fields: + + Property | Description --- | --- `description` | String that is hardcoded in the `state.ini` file and can be edited there. @@ -89,18 +91,11 @@ Property | Description `version.`*`.features.`* | Set of a features properties. For specific details for property meaning of a feature check documentation for specific API version. `version.`*`.features..required`* | If true the feature is required to be used by client. + + For each API version there is set of properties, e.g. for v1 in the example abovefeatures are: -- `authentiacation` - server has authentication feature enabled. -- `required` is an property of authentication feature and is required to be - used by a client. -- `methods` is a collection of supported authentication methods. In the - example above server only supports `basic-auth` which is for basic - access authentication (see - [TabPy Server Configuration Instructions](server-config.md) for how to - confire TabPy for authentication. - See [TabPy Configuration](#tabpy-configuration) section for more information on modifying the settings. diff --git a/startup.cmd b/startup.cmd index 4aba054c..5472dadd 100755 --- a/startup.cmd +++ b/startup.cmd @@ -9,7 +9,7 @@ SET SAVE_PYTHONPATH=%PYTHONPATH% ECHO Checking for presence of Python in the system path variable. -python --version >nul 2>&1 +python --version IF %ERRORLEVEL% NEQ 0 ( ECHO Cannot find Python.exe. Check that Python is installed and is in the system PATH environment variable. SET RET=1 diff --git a/tests/integration/resources/pwdfile.txt b/tests/integration/resources/pwdfile.txt new file mode 100755 index 00000000..1af7af4b --- /dev/null +++ b/tests/integration/resources/pwdfile.txt @@ -0,0 +1 @@ +user1 b8a63cf588cd2399da615042de4732f5b121c4d1042acfb91598a56d2dd3e1d7b9f785213262eddbbd00c7a8c9c3e89d7cb98f31d405cc644b1aeba92c3de40b diff --git a/tests/integration/test_auth.py b/tests/integration/test_auth.py new file mode 100755 index 00000000..f14f77c9 --- /dev/null +++ b/tests/integration/test_auth.py @@ -0,0 +1,141 @@ +import base64 +import http.client +import os +import platform +import shutil +import signal +import subprocess +import tempfile +import time +import unittest + + +class TestAuth(unittest.TestCase): + def setUp(self): + self.payload = ( + '''{ + "data": { "_arg1": [1, 2] }, + "script": "return [x * 2 for x in _arg1]" + }''') + + prefix = 'TabPyIntegTest' + self.tmp_dir = tempfile.mkdtemp(prefix=prefix) + + # create temporary state.ini + self.state_file = open(os.path.join(self.tmp_dir, 'state.ini'), 'w+') + self.state_file.write( + '[Service Info]\n' + 'Name = TabPy Serve\n' + 'Description = \n' + 'Creation Time = 0\n' + 'Access-Control-Allow-Origin = \n' + 'Access-Control-Allow-Headers = \n' + 'Access-Control-Allow-Methods = \n' + '\n' + '[Query Objects Service Versions]\n' + '\n' + '[Query Objects Docstrings]\n' + '\n' + '[Meta]\n' + 'Revision Number = 1\n') + self.state_file.close() + + # create config file + self.config_file = open(os.path.join(self.tmp_dir, 'auth.conf'), 'w+') + self.config_file.write( + '[TabPy]\n' + 'TABPY_PORT=9004\n' + 'TABPY_PWD_FILE=./tests/integration/resources/pwdfile.txt\n' + f'TABPY_STATE_PATH = {self.tmp_dir}') + self.config_file.close() + + # Platform specific - for integration tests we want to engage + # startup script + if platform.system() == 'Windows': + self.process = subprocess.Popen( + ['startup.cmd', self.config_file.name, '&']) + else: + self.process = subprocess.Popen( + ['./startup.sh', + '--config=' + self.config_file.name, '&'], + preexec_fn=os.setsid) + # give the app some time to start up... + time.sleep(3) + + def tearDown(self): + # stop TabPy + if platform.system() == 'Windows': + subprocess.call(['taskkill', '/F', '/T', '/PID', + str(self.process.pid)]) + else: + os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) + self.process.kill() + + # remove temporary files + os.remove(self.state_file.name) + os.remove(self.config_file.name) + shutil.rmtree(self.tmp_dir) + + def _get_connection(self) -> http.client.HTTPConnection: + connection = http.client.HTTPConnection('localhost:9004') + return connection + + def test_missing_credentials_fails(self): + headers = { + 'Content-Type': "application/json", + 'TabPy-Client': "Integration tests for Auth" + } + + conn = self._get_connection() + conn.request("POST", "/evaluate", self.payload, headers) + res = conn.getresponse() + + self.assertEqual(401, res.status) + + def test_invalid_password(self): + headers = { + 'Content-Type': "application/json", + 'TabPy-Client': "Integration tests for Auth", + 'Authorization': + 'Basic ' + + base64.b64encode('user1:wrong_password'.encode('utf-8')). + decode('utf-8') + } + + conn = self._get_connection() + conn.request("POST", "/evaluate", self.payload, headers) + res = conn.getresponse() + + self.assertEqual(401, res.status) + + def test_invalid_username(self): + headers = { + 'Content-Type': "application/json", + 'TabPy-Client': "Integration tests for Auth", + 'Authorization': + 'Basic ' + + base64.b64encode('wrong_user:P@ssw0rd'.encode('utf-8')). + decode('utf-8') + } + + conn = self._get_connection() + conn.request("POST", "/evaluate", self.payload, headers) + res = conn.getresponse() + + self.assertEqual(401, res.status) + + def test_valid_credentials(self): + headers = { + 'Content-Type': "application/json", + 'TabPy-Client': "Integration tests for Auth", + 'Authorization': + 'Basic ' + + base64.b64encode('user1:P@ssw0rd'.encode('utf-8')). + decode('utf-8') + } + + conn = self._get_connection() + conn.request("POST", "/evaluate", self.payload, headers) + res = conn.getresponse() + + self.assertEqual(200, res.status) diff --git a/tests/unit/server_tests/test_endpoint_handler.py b/tests/unit/server_tests/test_endpoint_handler.py index 6e276d29..f46a2e03 100755 --- a/tests/unit/server_tests/test_endpoint_handler.py +++ b/tests/unit/server_tests/test_endpoint_handler.py @@ -1,11 +1,9 @@ import base64 import os import tempfile -import unittest from argparse import Namespace from tabpy_server.app.app import TabPyApp -from tabpy_server.handlers.endpoint_handler import EndpointHandler from tabpy_server.handlers.util import hash_password from tornado.testing import AsyncHTTPTestCase from unittest.mock import patch diff --git a/tests/unit/server_tests/test_pwd_file.py b/tests/unit/server_tests/test_pwd_file.py index c431cae8..a7bfa719 100755 --- a/tests/unit/server_tests/test_pwd_file.py +++ b/tests/unit/server_tests/test_pwd_file.py @@ -1,14 +1,8 @@ -import logging -import pathlib import os import unittest -from argparse import Namespace from tempfile import NamedTemporaryFile from tabpy_server.app.app import TabPyApp -from tabpy_server.app.ConfigParameters import ConfigParameters - -from unittest.mock import patch, call class TestPasswordFile(unittest.TestCase): @@ -93,7 +87,7 @@ def test_given_username_but_no_password_expect_parsing_fails(self): "{} {}".format(login, pwd)) with self.assertRaises(RuntimeError) as cm: - app = TabPyApp(self.config_file.name) + _ = TabPyApp(self.config_file.name) ex = cm.exception self.assertEqual('Failed to read password file {}'.format( self.pwd_file.name), ex.args[0]) @@ -111,7 +105,7 @@ def test_given_duplicate_usernames_expect_parsing_fails(self): "{} {}\n{} {}".format(login, pwd, login, pwd)) with self.assertRaises(RuntimeError) as cm: - app = TabPyApp(self.config_file.name) + _ = TabPyApp(self.config_file.name) ex = cm.exception self.assertEqual('Failed to read password file {}'.format( self.pwd_file.name), ex.args[0]) From c17f4533cc9984eb5c5885ebc17af01da9e9f409 Mon Sep 17 00:00:00 2001 From: ogolovatyi Date: Tue, 23 Apr 2019 16:39:06 -0700 Subject: [PATCH 06/13] Split unit and integration tests run --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4c26b228..69e276ea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,8 @@ install: - pip install coveralls - npm install -g markdownlint-cli script: - - pytest tests --cov=tabpy-server/tabpy_server --cov=tabpy-tools/tabpy_tools --cov-append + - pytest tests/unit --cov=tabpy-server/tabpy_server --cov=tabpy-tools/tabpy_tools --cov-append + - pytest tests/integration - markdownlint . after_success: - coveralls From 798f5cd92ebe75cd52fc90b71b473c661bad883a Mon Sep 17 00:00:00 2001 From: ogolovatyi Date: Tue, 23 Apr 2019 16:41:35 -0700 Subject: [PATCH 07/13] Fix docs --- docs/api-v1.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/api-v1.md b/docs/api-v1.md index c2c963a0..e78b02f8 100755 --- a/docs/api-v1.md +++ b/docs/api-v1.md @@ -54,7 +54,7 @@ Property | Description -### http:get:: /status +## http:get:: /status Gets runtime status of deployed endpoints. If no endpoints are deployed in the server, the returned data is an empty JSON object. @@ -92,7 +92,7 @@ Using curl: curl -X GET http://localhost:9004/status ``` -### http:get:: /endpoints +## http:get:: /endpoints Gets a list of deployed endpoints and their static information. If no endpoints are deployed in the server, the returned data is an empty JSON object. @@ -138,7 +138,7 @@ Using curl: curl -X GET http://localhost:9004/endpoints ``` -### http:get:: /endpoints/:endpoint +## http:get:: /endpoints/:endpoint Gets the description of a specific deployed endpoint. The endpoint must first be deployed in the server (see the [TabPy Tools documentation](tabpy-tools.md)). @@ -168,7 +168,7 @@ Using curl: curl -X GET http://localhost:9004/endpoints/add ``` -### http:post:: /evaluate +## http:post:: /evaluate Executes a block of Python code, replacing named parameters with their provided values. @@ -239,7 +239,7 @@ curl -X POST http://localhost:9004/evaluate \ "script": "return tabpy.query(\"add\", x=_arg1, y=_arg2)[\"response\"]"}' ``` -### http:post:: /query/:endpoint +## http:post:: /query/:endpoint Executes a function at the specified endpoint. The function must first be deployed (see the [TabPy Tools documentation](tabpy-tools.md)). From 6a300559fcb87a5b3bc001ea8c8c23b5c2dc5aca Mon Sep 17 00:00:00 2001 From: ogolovatyi Date: Tue, 23 Apr 2019 16:41:51 -0700 Subject: [PATCH 08/13] Fix docs --- docs/api-v1.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/api-v1.md b/docs/api-v1.md index e78b02f8..9f73c7a8 100755 --- a/docs/api-v1.md +++ b/docs/api-v1.md @@ -1,18 +1,18 @@ # TabPy API v1 - - - - -- [Authentication](#authentication) - * [http:get:: /status](#httpget-status) - * [http:get:: /endpoints](#httpget-endpoints) - * [http:get:: /endpoints/:endpoint](#httpget-endpointsendpoint) - * [http:post:: /evaluate](#httppost-evaluate) - * [http:post:: /query/:endpoint](#httppost-queryendpoint) - - - + + + + +- [Authentication](#authentication) +- [http:get:: /status](#httpget-status) +- [http:get:: /endpoints](#httpget-endpoints) +- [http:get:: /endpoints/:endpoint](#httpget-endpointsendpoint) +- [http:post:: /evaluate](#httppost-evaluate) +- [http:post:: /query/:endpoint](#httppost-queryendpoint) + + + ## Authentication @@ -277,4 +277,4 @@ Using curl: curl -X GET http://localhost:9004/query/clustering -d \ '{"data": {"x": [6.35, 6.40, 6.65, 8.60, 8.90, 9.00, 9.10], "y": [1.95, 1.95, 2.05, 3.05, 3.05, 3.10, 3.15]}}' -``` +``` From 95d33934a99a62d50fea6e29f9268281ca94cc7c Mon Sep 17 00:00:00 2001 From: ogolovatyi Date: Tue, 23 Apr 2019 16:52:15 -0700 Subject: [PATCH 09/13] Modify PYTHONPATH in travis.yml --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index a46f7983..e8cbb03a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ install: - pip install coveralls - npm install -g markdownlint-cli script: + - export PYTHONPATH=${TABPY_ROOT}/tabpy-server:${TABPY_ROOT}/tabpy-tools:$PYTHONPATH - pytest tests/unit --cov=tabpy-server/tabpy_server --cov=tabpy-tools/tabpy_tools --cov-append - pytest tests/integration - markdownlint . From 048fbc2be15b1853d51aa3e3cc3aa2267f1eab5c Mon Sep 17 00:00:00 2001 From: ogolovatyi Date: Tue, 23 Apr 2019 16:55:09 -0700 Subject: [PATCH 10/13] Modify PYTHONPATH in travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e8cbb03a..669de4a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ install: - pip install coveralls - npm install -g markdownlint-cli script: - - export PYTHONPATH=${TABPY_ROOT}/tabpy-server:${TABPY_ROOT}/tabpy-tools:$PYTHONPATH + - export PYTHONPATH=./tabpy-server:./tabpy-tools:$PYTHONPATH - pytest tests/unit --cov=tabpy-server/tabpy_server --cov=tabpy-tools/tabpy_tools --cov-append - pytest tests/integration - markdownlint . From d184630d055f2f780f3726f1e4a1e152fb25c1e5 Mon Sep 17 00:00:00 2001 From: ogolovatyi Date: Wed, 24 Apr 2019 13:47:53 -0700 Subject: [PATCH 11/13] Add base class for integration tests --- CONTRIBUTING.md | 5 +- docs/api-v1.md | 2 +- docs/server-config.md | 4 +- docs/server-rest.md | 2 +- tests/integration/integ_test_base.py | 85 ++++++++++++++++++++++++++++ tests/integration/test_auth.py | 75 +----------------------- 6 files changed, 95 insertions(+), 78 deletions(-) create mode 100755 tests/integration/integ_test_base.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e670e3ef..e70240ba 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,8 +7,9 @@ - [Cloning TabPy Repository](#cloning-tabpy-repository) - [Setting Up Environment](#setting-up-environment) - [Unit Tests](#unit-tests) +- [Integration Tests](#integration-tests) - [Code Coverage](#code-coverage) -- [TabPy in Pythong Virtual Environment](#tabpy-in-pythong-virtual-environment) +- [TabPy in Python Virtual Environment](#tabpy-in-python-virtual-environment) - [Documentation Updates](#documentation-updates) - [TabPy with Swagger](#tabpy-with-swagger) - [Code styling](#code-styling) @@ -85,7 +86,7 @@ either for server or tools test, or even combined: pytest tests --cov=tabpy-server/tabpy_server --cov=tabpy-tools/tabpy_tools --cov-append ``` -## TabPy in Pythong Virtual Environment +## TabPy in Python Virtual Environment If you have downloaded Tabpy and would like to manually install Tabpy Server not using pip then follow the steps below diff --git a/docs/api-v1.md b/docs/api-v1.md index 9f73c7a8..2ba1f6e1 100755 --- a/docs/api-v1.md +++ b/docs/api-v1.md @@ -50,7 +50,7 @@ Property | Description --- | --- `required` | Authentication is never optional for client to use if it is mentioned in features list. `methods` | List of supported authentication methods with their properties. -`methods.basic-auth` | TabPy requires to use basic access authenticatio, see [TabPy Server Configuration Instructions](server-config.md#authentication) for how to configure authentication. +`methods.basic-auth` | TabPy requires to use basic access authentication, see [TabPy Server Configuration Instructions](server-config.md#authentication) for how to configure authentication. diff --git a/docs/server-config.md b/docs/server-config.md index a1042e97..b0d573d7 100755 --- a/docs/server-config.md +++ b/docs/server-config.md @@ -8,7 +8,7 @@ - [Authentication](#authentication) * [Enabling Authentication](#enabling-authentication) * [Password File](#password-file) - * [setting Up Environmnet](#setting-up-environmnet) + * [Setting Up Environmnet](#setting-up-environmnet) * [Adding an Account](#adding-an-account) * [Updating an Account](#updating-an-account) * [Deleting an Account](#deleting-an-account) @@ -95,7 +95,7 @@ see how to use it. After making any changes to the password file, TabPy needs to be restarted. -### setting Up Environmnet +### Setting Up Environmnet Before making any code changes run environment setup script. For Windows run the next command from the repository root folder: diff --git a/docs/server-rest.md b/docs/server-rest.md index de2490aa..902455d8 100755 --- a/docs/server-rest.md +++ b/docs/server-rest.md @@ -94,7 +94,7 @@ Property | Description For each API version there is set of properties, e.g. for v1 in the example -abovefeatures are: +above features are: See [TabPy Configuration](#tabpy-configuration) section for more information on modifying the settings. diff --git a/tests/integration/integ_test_base.py b/tests/integration/integ_test_base.py new file mode 100755 index 00000000..592b31fa --- /dev/null +++ b/tests/integration/integ_test_base.py @@ -0,0 +1,85 @@ +import http.client +import os +import platform +import shutil +import signal +import subprocess +import tempfile +import time +import unittest + + +class IntegTestBase(unittest.TestCase): + ''' + Base class for integration tests. + ''' + def __init__(self, methodName="runTest"): + super(IntegTestBase, self).__init__(methodName) + self.process = None + + def setUp(self): + super(IntegTestBase, self).setUp() + prefix = 'TabPyIntegTest' + self.tmp_dir = tempfile.mkdtemp(prefix=prefix) + + # create temporary state.ini + self.state_file = open(os.path.join(self.tmp_dir, 'state.ini'), 'w+') + self.state_file.write( + '[Service Info]\n' + 'Name = TabPy Serve\n' + 'Description = \n' + 'Creation Time = 0\n' + 'Access-Control-Allow-Origin = \n' + 'Access-Control-Allow-Headers = \n' + 'Access-Control-Allow-Methods = \n' + '\n' + '[Query Objects Service Versions]\n' + '\n' + '[Query Objects Docstrings]\n' + '\n' + '[Meta]\n' + 'Revision Number = 1\n') + self.state_file.close() + + # create config file + self.config_file = open(os.path.join(self.tmp_dir, 'auth.conf'), 'w+') + self.config_file.write( + '[TabPy]\n' + 'TABPY_PORT=9004\n' + 'TABPY_PWD_FILE=./tests/integration/resources/pwdfile.txt\n' + f'TABPY_STATE_PATH = {self.tmp_dir}') + self.config_file.close() + + # Platform specific - for integration tests we want to engage + # startup script + if platform.system() == 'Windows': + self.process = subprocess.Popen( + ['startup.cmd', self.config_file.name, '&']) + else: + self.process = subprocess.Popen( + ['./startup.sh', + '--config=' + self.config_file.name, '&'], + preexec_fn=os.setsid) + # give the app some time to start up... + time.sleep(3) + + def tearDown(self): + # stop TabPy + if self.process is not None: + if platform.system() == 'Windows': + subprocess.call(['taskkill', '/F', '/T', '/PID', + str(self.process.pid)]) + else: + os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) + self.process.kill() + + # remove temporary files + os.remove(self.state_file.name) + os.remove(self.config_file.name) + shutil.rmtree(self.tmp_dir) + + super(IntegTestBase, self).tearDown() + + def _get_connection(self) -> http.client.HTTPConnection: + connection = http.client.HTTPConnection('localhost:9004') + return connection diff --git a/tests/integration/test_auth.py b/tests/integration/test_auth.py index f14f77c9..54db5308 100755 --- a/tests/integration/test_auth.py +++ b/tests/integration/test_auth.py @@ -1,85 +1,16 @@ import base64 -import http.client -import os -import platform -import shutil -import signal -import subprocess -import tempfile -import time -import unittest +import integ_test_base -class TestAuth(unittest.TestCase): +class TestAuth(integ_test_base.IntegTestBase): def setUp(self): + super(TestAuth, self).setUp() self.payload = ( '''{ "data": { "_arg1": [1, 2] }, "script": "return [x * 2 for x in _arg1]" }''') - prefix = 'TabPyIntegTest' - self.tmp_dir = tempfile.mkdtemp(prefix=prefix) - - # create temporary state.ini - self.state_file = open(os.path.join(self.tmp_dir, 'state.ini'), 'w+') - self.state_file.write( - '[Service Info]\n' - 'Name = TabPy Serve\n' - 'Description = \n' - 'Creation Time = 0\n' - 'Access-Control-Allow-Origin = \n' - 'Access-Control-Allow-Headers = \n' - 'Access-Control-Allow-Methods = \n' - '\n' - '[Query Objects Service Versions]\n' - '\n' - '[Query Objects Docstrings]\n' - '\n' - '[Meta]\n' - 'Revision Number = 1\n') - self.state_file.close() - - # create config file - self.config_file = open(os.path.join(self.tmp_dir, 'auth.conf'), 'w+') - self.config_file.write( - '[TabPy]\n' - 'TABPY_PORT=9004\n' - 'TABPY_PWD_FILE=./tests/integration/resources/pwdfile.txt\n' - f'TABPY_STATE_PATH = {self.tmp_dir}') - self.config_file.close() - - # Platform specific - for integration tests we want to engage - # startup script - if platform.system() == 'Windows': - self.process = subprocess.Popen( - ['startup.cmd', self.config_file.name, '&']) - else: - self.process = subprocess.Popen( - ['./startup.sh', - '--config=' + self.config_file.name, '&'], - preexec_fn=os.setsid) - # give the app some time to start up... - time.sleep(3) - - def tearDown(self): - # stop TabPy - if platform.system() == 'Windows': - subprocess.call(['taskkill', '/F', '/T', '/PID', - str(self.process.pid)]) - else: - os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) - self.process.kill() - - # remove temporary files - os.remove(self.state_file.name) - os.remove(self.config_file.name) - shutil.rmtree(self.tmp_dir) - - def _get_connection(self) -> http.client.HTTPConnection: - connection = http.client.HTTPConnection('localhost:9004') - return connection - def test_missing_credentials_fails(self): headers = { 'Content-Type': "application/json", From a19ff6ed41e1af70fca69f55a474682fbe9a2584 Mon Sep 17 00:00:00 2001 From: ogolovatyi Date: Wed, 24 Apr 2019 16:24:10 -0700 Subject: [PATCH 12/13] Integration tests for unknown URL --- .vscode/settings.json | 10 +- tests/integration/integ_test_base.py | 203 ++++++++++++++++-- .../resources/2019_04_24_to_3018_08_25.crt | 23 ++ .../resources/2019_04_24_to_3018_08_25.key | 27 +++ tests/integration/test_auth.py | 3 + tests/integration/test_url.py | 14 ++ tests/integration/test_url_ssl.py | 28 +++ 7 files changed, 282 insertions(+), 26 deletions(-) create mode 100644 tests/integration/resources/2019_04_24_to_3018_08_25.crt create mode 100644 tests/integration/resources/2019_04_24_to_3018_08_25.key create mode 100755 tests/integration/test_url.py create mode 100755 tests/integration/test_url_ssl.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 569c1d56..d730aa81 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,11 +7,11 @@ "python.linting.pylintEnabled": false, "python.linting.flake8Enabled": true, "python.linting.enabled": true, - "python.unitTest.autoTestDiscoverOnSaveEnabled": true, - "python.unitTest.pyTestArgs": [ + "python.testing.autoTestDiscoverOnSaveEnabled": true, + "python.testing.pyTestArgs": [ "tests" ], - "python.unitTest.unittestEnabled": false, - "python.unitTest.nosetestsEnabled": false, - "python.unitTest.pyTestEnabled": true + "python.testing.unittestEnabled": true, + "python.testing.nosetestsEnabled": false, + "python.testing.pyTestEnabled": true } \ No newline at end of file diff --git a/tests/integration/integ_test_base.py b/tests/integration/integ_test_base.py index 592b31fa..c85620d4 100755 --- a/tests/integration/integ_test_base.py +++ b/tests/integration/integ_test_base.py @@ -16,15 +16,34 @@ class IntegTestBase(unittest.TestCase): def __init__(self, methodName="runTest"): super(IntegTestBase, self).__init__(methodName) self.process = None + self.delete_temp_folder = True - def setUp(self): - super(IntegTestBase, self).setUp() - prefix = 'TabPyIntegTest' - self.tmp_dir = tempfile.mkdtemp(prefix=prefix) + def set_delete_temp_folder(self, delete_temp_folder: bool): + ''' + Specify if temporary folder for state, config and log + files should be deleted when test is done. + By default the folder is deleted. - # create temporary state.ini - self.state_file = open(os.path.join(self.tmp_dir, 'state.ini'), 'w+') - self.state_file.write( + Parameters + ---------- + delete_test_folder: bool + If True temp folder will be deleted. + ''' + self.delete_temp_folder = delete_temp_folder + + def _get_state_file_path(self) -> str: + ''' + Generates state.ini and returns absolute path to it. + Overwrite this function for tests to run against not default state + file. + + Returns + ------- + str + Absolute path to state file folder. + ''' + state_file = open(os.path.join(self.tmp_dir, 'state.ini'), 'w+') + state_file.write( '[Service Info]\n' 'Name = TabPy Serve\n' 'Description = \n' @@ -39,26 +58,158 @@ def setUp(self): '\n' '[Meta]\n' 'Revision Number = 1\n') - self.state_file.close() + state_file.close() - # create config file - self.config_file = open(os.path.join(self.tmp_dir, 'auth.conf'), 'w+') - self.config_file.write( + return self.tmp_dir + + def _get_port(self) -> str: + ''' + Returns port TabPy should run on. Default implementation + returns '9004'. + + Returns + ------- + str + Port number. + ''' + return '9004' + + def _get_pwd_file(self) -> str: + ''' + Returns absolute or relative path to password file. + Overwrite to create and/or specify your own file. + Default implementation returns None which means + TABPY_PWD_FILE setting won't be added to config. + + Returns + ------- + str + Absolute or relative path to password file. + If None TABPY_PWD_FILE setting won't be added to + config. + ''' + return None + + def _get_transfer_protocol(self) -> str: + ''' + Returns transfer protocol for configuration file. + Default implementation returns None which means + TABPY_TRANSFER_PROTOCOL setting won't be added to config. + + Returns + ------- + str + Transfer protocol (e.g 'http' or 'https'). + If None TABPY_TRANSFER_PROTOCOL setting won't be + added to config. + ''' + return None + + def _get_certificate_file_name(self) -> str: + ''' + Returns absolute or relative certificate file name + for configuration file. + Default implementation returns None which means + TABPY_CERTIFICATE_FILE setting won't be added to config. + + Returns + ------- + str + Absolute or relative certificate file name. + If None TABPY_CERTIFICATE_FILE setting won't be + added to config. + ''' + return None + + def _get_key_file_name(self) -> str: + ''' + Returns absolute or relative private key file name + for configuration file. + Default implementation returns None which means + TABPY_KEY_FILE setting won't be added to config. + + Returns + ------- + str + Absolute or relative private key file name. + If None TABPY_KEY_FILE setting won't be + added to config. + ''' + return None + + def _get_config_file_name(self) -> str: + ''' + Generates config file. Overwrite this function for tests to + run against not default state file. + + Returns + ------- + str + Absolute path to config file. + ''' + config_file = open(os.path.join(self.tmp_dir, 'test.conf'), 'w+') + config_file.write( '[TabPy]\n' - 'TABPY_PORT=9004\n' - 'TABPY_PWD_FILE=./tests/integration/resources/pwdfile.txt\n' - f'TABPY_STATE_PATH = {self.tmp_dir}') - self.config_file.close() + f'TABPY_PORT={self._get_port()}\n' + f'TABPY_STATE_PATH = {self.tmp_dir}\n') + + pwd_file = self._get_pwd_file() + if pwd_file is not None: + pwd_file = os.path.abspath(pwd_file) + config_file.write(f'TABPY_PWD_FILE={pwd_file}\n') + + transfer_protocol = self._get_transfer_protocol() + if transfer_protocol is not None: + config_file.write(f'TABPY_TRANSFER_PROTOCOL={transfer_protocol}\n') + + cert_file_name = self._get_certificate_file_name() + if cert_file_name is not None: + cert_file_name = os.path.abspath(cert_file_name) + config_file.write(f'TABPY_CERTIFICATE_FILE={cert_file_name}\n') + + key_file_name = self._get_key_file_name() + if key_file_name is not None: + key_file_name = os.path.abspath(key_file_name) + config_file.write(f'TABPY_KEY_FILE={key_file_name}\n') + + config_file.close() + + self.delete_config_file = True + return config_file.name + + def setUp(self): + super(IntegTestBase, self).setUp() + prefix = 'TabPy_IntegTest_' + self.tmp_dir = tempfile.mkdtemp(prefix=prefix) + + # create temporary state.ini + orig_state_file_name = os.path.abspath( + self._get_state_file_path() + '/state.ini') + self.state_file_name = os.path.abspath(self.tmp_dir + '/state.ini') + if orig_state_file_name != self.state_file_name: + shutil.copyfile(orig_state_file_name, self.state_file_name) + + # create config file + orig_config_file_name = os.path.abspath(self._get_config_file_name()) + self.config_file_name = os.path.abspath( + self.tmp_dir + '/' + + os.path.basename(orig_config_file_name)) + if orig_config_file_name != self.config_file_name: + shutil.copyfile(orig_config_file_name, self.config_file_name) # Platform specific - for integration tests we want to engage # startup script if platform.system() == 'Windows': self.process = subprocess.Popen( - ['startup.cmd', self.config_file.name, '&']) + ['startup.cmd', + self.config_file_name, + f'>{self.tmp_dir}/log.txt', + '2>&1', + '&']) else: self.process = subprocess.Popen( ['./startup.sh', - '--config=' + self.config_file.name, '&'], + '--config=' + self.config_file_name, '&'], preexec_fn=os.setsid) # give the app some time to start up... time.sleep(3) @@ -73,13 +224,23 @@ def tearDown(self): os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) self.process.kill() + # after shutting down TabPy and before we start it again + # for next test give it some time to terminate... + time.sleep(1) + # remove temporary files - os.remove(self.state_file.name) - os.remove(self.config_file.name) - shutil.rmtree(self.tmp_dir) + if self.delete_state_file: + os.remove(self.state_file_name) + os.remove(self.config_file_name) + shutil.rmtree(self.tmp_dir) super(IntegTestBase, self).tearDown() def _get_connection(self) -> http.client.HTTPConnection: - connection = http.client.HTTPConnection('localhost:9004') + url = '' + protocol = self._get_transfer_protocol() + if protocol is not None: + url = protocol + '://' + url += 'localhost:' + self._get_port() + connection = http.client.HTTPConnection(url) return connection diff --git a/tests/integration/resources/2019_04_24_to_3018_08_25.crt b/tests/integration/resources/2019_04_24_to_3018_08_25.crt new file mode 100644 index 00000000..69bf52ee --- /dev/null +++ b/tests/integration/resources/2019_04_24_to_3018_08_25.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDyjCCArICCQD3+Tk53RbuzTANBgkqhkiG9w0BAQsFADCBpTELMAkGA1UEBhMC +VVMxCzAJBgNVBAgMAldBMREwDwYDVQQHDAhLaXJrbGFuZDEZMBcGA1UECgwQVGFi +bGVhdSBTb2Z0d2FyZTEbMBkGA1UECwwSQWR2YW5jZWQgQW5hbHl0aWNzMRcwFQYD +VQQDDA5PbGVrIEdvbG92YXR5aTElMCMGCSqGSIb3DQEJARYWb2dvbG92YXR5aUB0 +YWJsZWF1LmNvbTAgFw0xOTA0MjQyMTU1NDVaGA8zMDE4MDgyNTIxNTU0NVowgaUx +CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJXQTERMA8GA1UEBwwIS2lya2xhbmQxGTAX +BgNVBAoMEFRhYmxlYXUgU29mdHdhcmUxGzAZBgNVBAsMEkFkdmFuY2VkIEFuYWx5 +dGljczEXMBUGA1UEAwwOT2xlayBHb2xvdmF0eWkxJTAjBgkqhkiG9w0BCQEWFm9n +b2xvdmF0eWlAdGFibGVhdS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCnoOgC0w3mSS2uoRQOcKtkC3ueHxo8hDXsdnBaCdcvo8ixqvYiKP/twZCb +sz+5YGFGkCwGWrdX9U9Iy/70r1fLyoZ89oswjf4ei3FczFfjTB1l4pgnDBYKWgQm +IdkZ3n26YmNWm/4e3cm61KYY8fJN0v9Ql5NBxH+xRrvwqgkFRZJcIuAEa7k28FD/ +KaMLOgDMxtuFXcoQSwT75ggmhM89aeE4kKf+MbG7dkwoV3y1hZG/gW6BryLfo2xA +YlaQwtzPBPhgE8gsqxtO7l+wxv03JOnkPQNBWHAf7MtlkqdM6g03UrWmfTFhqzPE +rzsiWOXDxD2c5HSiss24HHrgF37rAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIQb +TwPcrDeYUwIz8TWEEcIX3jJoDMyR6Q+AEmM8C8ed0JlavE2qPiZc+lLr8Dc1B+fE +7UkxHPZOw0fCtrQ3I3+0Z/6YfqZs3m1f1I8Yr6SSW6NjAj7+mmMQ8DuJGb5yuefP +Z0LV8F+OwUXNI7bsl0Q4UKt8QQ0ovI3I6w8HVsuy7zyEUN268tiK58bMkSfbzVal +UvIcXqZyBFKQ/ZZ3BknI8b3ibya7h7R/92CsMDfPAQASZcBwKJ64RW9Wi5Gzzqmp +D2Vk6MdyOgp4bD9wDqm4f6p20FewagSL5/c3lk1EjoCye6UAH2cnqPRTowI2elJg +W9mrYH2k9L2cnnUIyx4= +-----END CERTIFICATE----- diff --git a/tests/integration/resources/2019_04_24_to_3018_08_25.key b/tests/integration/resources/2019_04_24_to_3018_08_25.key new file mode 100644 index 00000000..bd8c448e --- /dev/null +++ b/tests/integration/resources/2019_04_24_to_3018_08_25.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAp6DoAtMN5kktrqEUDnCrZAt7nh8aPIQ17HZwWgnXL6PIsar2 +Iij/7cGQm7M/uWBhRpAsBlq3V/VPSMv+9K9Xy8qGfPaLMI3+HotxXMxX40wdZeKY +JwwWCloEJiHZGd59umJjVpv+Ht3JutSmGPHyTdL/UJeTQcR/sUa78KoJBUWSXCLg +BGu5NvBQ/ymjCzoAzMbbhV3KEEsE++YIJoTPPWnhOJCn/jGxu3ZMKFd8tYWRv4Fu +ga8i36NsQGJWkMLczwT4YBPILKsbTu5fsMb9NyTp5D0DQVhwH+zLZZKnTOoNN1K1 +pn0xYaszxK87Iljlw8Q9nOR0orLNuBx64Bd+6wIDAQABAoIBACsVRAxVymDBtigH +5mu/sY1JFkCRpeCf6mwYFNBPbysjYVWopxIoj37AHTanX1151Aaaz3XiovTMa9A9 +/g1Nc7dBGkfL5gJYvFOFa2F6c6xLx9KD5q9Cf/exIxfZ4z6u3Imm9/kupqWwQ0Tt +mrMWnDw8WrqP+p0Qr/EUSQGV8jOUP3SyA3zQbEbsBkettz4Nil9NITwbQrbnG6o7 +RnNuxCwRwO3QB1p0YOXnKytU8xIu78Xg3qwVx81QP3/omB9jNCY52p1FRLzGO21q +JFZLIcP7hECesn5v9dZa99oChU+Rdzi93VEymwmZBVl8givNdWAe1Y7c/Z8fIWS/ +FQVvRgECgYEA12uZRlLDnDoqYTj5Ots87Kpy15wU5lA2ASGNgaQLXZBgh8ID/8o5 +QRk1/CNW2i/cW/fI7XRaiFoHjrvD6XgT0m/bWOezFWXoemkMR/jR2kYZjS0vpc04 +wd/rvrHr4nbSQE1cVBwLrYzrYJ6qrGybx9P6k0fhu2fUIJIJGEOTQ0ECgYEAxzSa +f4KLGSrZjfZl695z+83VUG5aeg8V407nA6RBw3XeK9BjHhqfsRnFAfvhtyTolm3M +QOaZLhSnhnW5HSdYW0QEtW4Lb5GkiGdZSArjM5zD/MgHitlOm9r0IL+nBbtNQYrd +i85pTeeIlG7CTFGtx3b9EiQYHYl2xeS3QbblcysCgYBvQVPk3OPHsMaodZtKSWY6 +uIEdV6/3jt+FUAXcOZPhG6qvEoWsOo29UD7wXHQDtYoyOVOdR2VmXFDg55pz3p8m +JLz9OpTj7UDWz6AXH6uJ9oBFyFt+XvH8NyBy2UMBL+rAaPPRQLbLSCdcPDXbXTBL +UPBt1kb/2czVkXZ/AI9ywQKBgQCGgsm0QhTk6J9AkdmenHZa2FEq32kutFMGSzgI +qHhToJploW/cWwPr1UfHICr4vO5k7T0Xsd5LVF0OmR1nRzMNZW98hxMnwgOEq6yI +zfk+16MrZHJbWoMPEJj6KA+C+kefc0JH7hgDJ8181RFT8W9TmdAm2MKD51eRJvBr +ajGjQwKBgQCvNf8Ds4Smy/5ANyOjK3/iPZiGiVgyaKCJOKNHQ+pAD0JS8XfOh/Km +KiXv8jBEQcChB7YoYKBUfXwpSLFruJU3kCLvN4MHQAgV0BfVx8MkcLJh6K+wMGPX +Es5hj6r4RQQblJaj9q8qb3+9uG3k7Sn4TXc0TYg7ml32ugXSXMxfKg== +-----END RSA PRIVATE KEY----- diff --git a/tests/integration/test_auth.py b/tests/integration/test_auth.py index 54db5308..c730d8ba 100755 --- a/tests/integration/test_auth.py +++ b/tests/integration/test_auth.py @@ -11,6 +11,9 @@ def setUp(self): "script": "return [x * 2 for x in _arg1]" }''') + def _get_pwd_file(self) -> str: + return './tests/integration/resources/pwdfile.txt' + def test_missing_credentials_fails(self): headers = { 'Content-Type': "application/json", diff --git a/tests/integration/test_url.py b/tests/integration/test_url.py new file mode 100755 index 00000000..5aab4628 --- /dev/null +++ b/tests/integration/test_url.py @@ -0,0 +1,14 @@ +''' +All other misc. URL-related integration tests. +''' + +import integ_test_base + + +class TestURL(integ_test_base.IntegTestBase): + def test_notexistant_url(self): + conn = self._get_connection() + conn.request("GET", "/unicorn") + res = conn.getresponse() + + self.assertEqual(404, res.status) diff --git a/tests/integration/test_url_ssl.py b/tests/integration/test_url_ssl.py new file mode 100755 index 00000000..b88a6aad --- /dev/null +++ b/tests/integration/test_url_ssl.py @@ -0,0 +1,28 @@ +''' +All other misc. URL-related integration tests for +when SSL is turned on for TabPy. +''' + +import integ_test_base + + +class TestURL_SSL(integ_test_base.IntegTestBase): + def _get_port(self) -> str: + return '9005' + + def _get_transfer_protocol(self) -> str: + return 'https' + + def _get_certificate_file_name(self) -> str: + return './tests/integration/resources/2019_04_24_to_3018_08_25.crt' + + def _get_key_file_name(self) -> str: + return './tests/integration/resources/2019_04_24_to_3018_08_25.key' + + def test_notexistant_url(self): + self.set_delete_temp_folder(False) + conn = self._get_connection() + conn.request("GET", "/unicorn") + res = conn.getresponse() + + self.assertEqual(404, res.status) From 399586a400084e40bd60efebebb40a092de9ca59 Mon Sep 17 00:00:00 2001 From: ogolovatyi Date: Thu, 25 Apr 2019 14:50:04 -0700 Subject: [PATCH 13/13] Add SSL integ test --- .vscode/settings.json | 9 ++-- CONTRIBUTING.md | 12 +++-- startup.sh | 2 +- tabpy-server/tabpy_server/app/app.py | 2 +- tests/integration/integ_test_base.py | 61 +++++++++++++----------- tests/integration/test_url.py | 2 +- tests/integration/test_url_ssl.py | 18 ++++--- tests/unit/server_tests/test_pwd_file.py | 4 +- 8 files changed, 63 insertions(+), 47 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index d730aa81..1f1c7bb0 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,10 +2,12 @@ "git.enabled": true, "files.exclude": { "**/__pycache__": true, - "**/.pytest_cache": true + "**/.pytest_cache": true, + "**/*.egg-info": true, + "**/*.pyc": true }, "python.linting.pylintEnabled": false, - "python.linting.flake8Enabled": true, + "python.linting.flake8Enabled": false, "python.linting.enabled": true, "python.testing.autoTestDiscoverOnSaveEnabled": true, "python.testing.pyTestArgs": [ @@ -13,5 +15,6 @@ ], "python.testing.unittestEnabled": true, "python.testing.nosetestsEnabled": false, - "python.testing.pyTestEnabled": true + "python.testing.pyTestEnabled": true, + "python.linting.pep8Enabled": true } \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e70240ba..d679edec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,7 +75,11 @@ Check `pytest` documentation for how to run individual tests or set of tests. ## Integration Tests -... +Integration tests can be executed with the next command: + +```sh +pytest tests/integration +``` ## Code Coverage @@ -88,9 +92,9 @@ pytest tests --cov=tabpy-server/tabpy_server --cov=tabpy-tools/tabpy_tools --cov ## TabPy in Python Virtual Environment -If you have downloaded Tabpy and would like to manually install Tabpy Server -not using pip then follow the steps below -[to run TabPy in Python virtual environment](docs/tabpy-virtualenv.md). +It is possible (and recommended) to run TabPy in a virtual environment, more +details are on +[TabPy in Python virtual environment](docs/tabpy-virtualenv.md) page. ## Documentation Updates diff --git a/startup.sh b/startup.sh index 04633074..c511ac62 100755 --- a/startup.sh +++ b/startup.sh @@ -27,7 +27,7 @@ function install_dependencies() { # Check for Python in PATH echo Checking for presence of Python in the system path variable. -python --version &>- +python3 --version check_status "Cannot find Python. Check that Python is installed and is in the system PATH environment variable." # Setting local variables diff --git a/tabpy-server/tabpy_server/app/app.py b/tabpy-server/tabpy_server/app/app.py index fe6e4122..96a48f12 100644 --- a/tabpy-server/tabpy_server/app/app.py +++ b/tabpy-server/tabpy_server/app/app.py @@ -232,7 +232,7 @@ def set_parameter(settings_key, self.settings[SettingsParameters.StaticPath] =\ os.path.abspath(self.settings[SettingsParameters.StaticPath]) logger.debug(f'Static pages folder set to ' - '"{self.settings[SettingsParameters.StaticPath]}"') + f'"{self.settings[SettingsParameters.StaticPath]}"') # Set subdirectory from config if applicable if tabpy_state.has_option("Service Info", "Subdirectory"): diff --git a/tests/integration/integ_test_base.py b/tests/integration/integ_test_base.py index c85620d4..220b56dc 100755 --- a/tests/integration/integ_test_base.py +++ b/tests/integration/integ_test_base.py @@ -150,27 +150,28 @@ def _get_config_file_name(self) -> str: config_file = open(os.path.join(self.tmp_dir, 'test.conf'), 'w+') config_file.write( '[TabPy]\n' - f'TABPY_PORT={self._get_port()}\n' + f'TABPY_PORT = {self._get_port()}\n' f'TABPY_STATE_PATH = {self.tmp_dir}\n') pwd_file = self._get_pwd_file() if pwd_file is not None: pwd_file = os.path.abspath(pwd_file) - config_file.write(f'TABPY_PWD_FILE={pwd_file}\n') + config_file.write(f'TABPY_PWD_FILE = {pwd_file}\n') transfer_protocol = self._get_transfer_protocol() if transfer_protocol is not None: - config_file.write(f'TABPY_TRANSFER_PROTOCOL={transfer_protocol}\n') + config_file.write( + f'TABPY_TRANSFER_PROTOCOL = {transfer_protocol}\n') cert_file_name = self._get_certificate_file_name() if cert_file_name is not None: cert_file_name = os.path.abspath(cert_file_name) - config_file.write(f'TABPY_CERTIFICATE_FILE={cert_file_name}\n') + config_file.write(f'TABPY_CERTIFICATE_FILE = {cert_file_name}\n') key_file_name = self._get_key_file_name() if key_file_name is not None: key_file_name = os.path.abspath(key_file_name) - config_file.write(f'TABPY_KEY_FILE={key_file_name}\n') + config_file.write(f'TABPY_KEY_FILE = {key_file_name}\n') config_file.close() @@ -199,20 +200,22 @@ def setUp(self): # Platform specific - for integration tests we want to engage # startup script - if platform.system() == 'Windows': - self.process = subprocess.Popen( - ['startup.cmd', - self.config_file_name, - f'>{self.tmp_dir}/log.txt', - '2>&1', - '&']) - else: - self.process = subprocess.Popen( - ['./startup.sh', - '--config=' + self.config_file_name, '&'], - preexec_fn=os.setsid) - # give the app some time to start up... - time.sleep(3) + with open(self.tmp_dir + '/output.txt', 'w') as outfile: + if platform.system() == 'Windows': + self.process = subprocess.Popen( + ['startup.cmd', self.config_file_name], + stdout=outfile, + stderr=outfile) + else: + self.process = subprocess.Popen( + ['./startup.sh', + '--config=' + self.config_file_name], + preexec_fn=os.setsid, + stdout=outfile, + stderr=outfile) + + # give the app some time to start up... + time.sleep(5) def tearDown(self): # stop TabPy @@ -224,12 +227,12 @@ def tearDown(self): os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) self.process.kill() - # after shutting down TabPy and before we start it again - # for next test give it some time to terminate... - time.sleep(1) + # after shutting down TabPy and before we start it again + # for next test give it some time to terminate. + time.sleep(5) # remove temporary files - if self.delete_state_file: + if self.delete_temp_folder: os.remove(self.state_file_name) os.remove(self.config_file_name) shutil.rmtree(self.tmp_dir) @@ -237,10 +240,12 @@ def tearDown(self): super(IntegTestBase, self).tearDown() def _get_connection(self) -> http.client.HTTPConnection: - url = '' protocol = self._get_transfer_protocol() - if protocol is not None: - url = protocol + '://' - url += 'localhost:' + self._get_port() - connection = http.client.HTTPConnection(url) + url = 'localhost:' + self._get_port() + + if protocol is not None and protocol.lower() == 'https': + connection = http.client.HTTPSConnection(url) + else: + connection = http.client.HTTPConnection(url) + return connection diff --git a/tests/integration/test_url.py b/tests/integration/test_url.py index 5aab4628..74785d88 100755 --- a/tests/integration/test_url.py +++ b/tests/integration/test_url.py @@ -6,7 +6,7 @@ class TestURL(integ_test_base.IntegTestBase): - def test_notexistant_url(self): + def test_notexistent_url(self): conn = self._get_connection() conn.request("GET", "/unicorn") res = conn.getresponse() diff --git a/tests/integration/test_url_ssl.py b/tests/integration/test_url_ssl.py index b88a6aad..0f300b46 100755 --- a/tests/integration/test_url_ssl.py +++ b/tests/integration/test_url_ssl.py @@ -4,10 +4,11 @@ ''' import integ_test_base +import requests class TestURL_SSL(integ_test_base.IntegTestBase): - def _get_port(self) -> str: + def _get_port(self): return '9005' def _get_transfer_protocol(self) -> str: @@ -19,10 +20,13 @@ def _get_certificate_file_name(self) -> str: def _get_key_file_name(self) -> str: return './tests/integration/resources/2019_04_24_to_3018_08_25.key' - def test_notexistant_url(self): - self.set_delete_temp_folder(False) - conn = self._get_connection() - conn.request("GET", "/unicorn") - res = conn.getresponse() + def test_notexistent_url(self): + session = requests.Session() + # Do not verify servers' cert to be signed by trusted CA + session.verify = False + # Do not warn about insecure request + requests.packages.urllib3.disable_warnings() + response = session.get( + url=f'https://localhost:{self._get_port()}/unicorn') - self.assertEqual(404, res.status) + self.assertEqual(404, response.status_code) diff --git a/tests/unit/server_tests/test_pwd_file.py b/tests/unit/server_tests/test_pwd_file.py index a7bfa719..e7132d4f 100755 --- a/tests/unit/server_tests/test_pwd_file.py +++ b/tests/unit/server_tests/test_pwd_file.py @@ -87,7 +87,7 @@ def test_given_username_but_no_password_expect_parsing_fails(self): "{} {}".format(login, pwd)) with self.assertRaises(RuntimeError) as cm: - _ = TabPyApp(self.config_file.name) + TabPyApp(self.config_file.name) ex = cm.exception self.assertEqual('Failed to read password file {}'.format( self.pwd_file.name), ex.args[0]) @@ -105,7 +105,7 @@ def test_given_duplicate_usernames_expect_parsing_fails(self): "{} {}\n{} {}".format(login, pwd, login, pwd)) with self.assertRaises(RuntimeError) as cm: - _ = TabPyApp(self.config_file.name) + TabPyApp(self.config_file.name) ex = cm.exception self.assertEqual('Failed to read password file {}'.format( self.pwd_file.name), ex.args[0])