diff --git a/.scrutinizer.yml b/.scrutinizer.yml index eb1c0c9e..d8a1c64c 100755 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -25,6 +25,7 @@ build: - command: pylint-run use_website_config: true + tests: true checks: python: code_rating: true diff --git a/CHANGELOG b/CHANGELOG index ad197662..e3987150 100755 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,16 @@ # Changelog -## v1.2.0 +## v2.0.0 + +### Breaking changes + +- TabPy fails with 400 when it is not configure for authentication + but credentials are provided by client. + +### Bug fixes + +- When TabPy is running with no console attached it is not failing + with 500 when trying to respond with 401 status. ### Improvements diff --git a/misc/TabPy.postman_collection.json b/misc/TabPy.postman_collection.json index fb796356..9769f299 100755 --- a/misc/TabPy.postman_collection.json +++ b/misc/TabPy.postman_collection.json @@ -6,19 +6,16 @@ }, "item": [ { - "name": "{{endpoint}}/info", + "name": "{{host}}:{{port}}/info", "request": { "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { - "raw": "{{endpoint}}/info", + "raw": "{{host}}:{{port}}/info", "host": [ - "{{endpoint}}" + "{{host}}" ], + "port": "{{port}}", "path": [ "info" ] @@ -27,8 +24,23 @@ "response": [] }, { - "name": "{{endpoint}}/evaluate", + "name": "{{host}}:{{port}}/evaluate", "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "P@ssw0rd", + "type": "string" + }, + { + "key": "username", + "value": "user1", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -36,19 +48,45 @@ "name": "Content-Type", "value": "application/json", "type": "text" + }, + { + "key": "TabPy-Client", + "value": "Postman for manual testing", + "type": "text" + }, + { + "key": "TabPy-User", + "value": "ogolovatyi", + "type": "text" } ], "body": { "mode": "raw", - "raw": "{\n\t\"data\": \n\t{ \n\t\t\"_arg1\" : [1, 2, 3], \n\t\t\"_arg2\" : [3, -1, 5]\n\t},\n\t\"script\": \n\t\"res = []\\nfor i in range(len(_arg1)):\\n res.append(_arg1[i] * _arg2[i])\\nreturn res\"\n}\n" + "raw": "{\n\t\"data\": \n\t{ \n\t\t\"_arg1\" : [1, 2, 3], \n\t\t\"_arg2\" : [3, -1, 5]\n\t},\n\t\"script\": \n\t\"return [x + y for x, y in zip(_arg1, _arg2)]\"\n}\n", + "options": { + "raw": {} + } }, "url": { - "raw": "{{endpoint}}/evaluate", + "raw": "{{host}}:{{port}}/evaluate", "host": [ - "{{endpoint}}" + "{{host}}" ], + "port": "{{port}}", "path": [ "evaluate" + ], + "query": [ + { + "key": "TabPy-Client", + "value": "Postman for Manual Testing", + "disabled": true + }, + { + "key": "TabPy-User", + "value": "ogolovatyi", + "disabled": true + } ] } }, @@ -59,15 +97,12 @@ "request": { "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { - "raw": "{{endpoint}}/status", + "raw": "{{host}}:{{port}}/status", "host": [ - "{{endpoint}}" + "{{host}}" ], + "port": "{{port}}", "path": [ "status" ] @@ -76,19 +111,16 @@ "response": [] }, { - "name": "{{endpoint}}/endpoints", + "name": "{{host}}:{{port}}/endpoints", "request": { "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { - "raw": "{{endpoint}}/endpoints", + "raw": "{{host}}:{{port}}/endpoints", "host": [ - "{{endpoint}}" + "{{host}}" ], + "port": "{{port}}", "path": [ "endpoints" ] @@ -97,7 +129,7 @@ "response": [] }, { - "name": "{{endpoint}}/query/add", + "name": "{{host}}:{{port}}/query/model_name", "request": { "method": "POST", "header": [ @@ -113,35 +145,33 @@ "raw": "{\r\n \"data\": {\r\n \"x\": [\r\n 6.35,\r\n 6.4,\r\n 6.65,\r\n 8.6,\r\n 8.9,\r\n 9,\r\n 9.1\r\n ],\r\n \"y\": [\r\n 1.95,\r\n 1.95,\r\n 2.05,\r\n 3.05,\r\n 3.05,\r\n 3.1,\r\n 3.15\r\n ]\r\n }\r\n}" }, "url": { - "raw": "{{endpoint}}/query/add", + "raw": "{{host}}:{{port}}/query/model_name", "host": [ - "{{endpoint}}" + "{{host}}" ], + "port": "{{port}}", "path": [ "query", - "add" + "model_name" ] } }, "response": [] }, { - "name": "{{endpoint}}/endpoints/add", + "name": "{{host}}:{{port}}/endpoints/model_name", "request": { "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { - "raw": "{{endpoint}}/endpoints/add", + "raw": "{{host}}:{{port}}/endpoints/model_name", "host": [ - "{{endpoint}}" + "{{host}}" ], + "port": "{{port}}", "path": [ "endpoints", - "add" + "model_name" ] } }, diff --git a/tabpy/VERSION b/tabpy/VERSION index 867e5243..359a5b95 100755 --- a/tabpy/VERSION +++ b/tabpy/VERSION @@ -1 +1 @@ -1.2.0 \ No newline at end of file +2.0.0 \ No newline at end of file diff --git a/tabpy/tabpy_server/common/default.conf b/tabpy/tabpy_server/common/default.conf index ee02453d..01aa274e 100644 --- a/tabpy/tabpy_server/common/default.conf +++ b/tabpy/tabpy_server/common/default.conf @@ -47,7 +47,7 @@ propagete=0 [handler_rootHandler] class=StreamHandler -level=DEBUG +level=INFO formatter=rootFormatter args=(sys.stdout,) diff --git a/tabpy/tabpy_server/handlers/base_handler.py b/tabpy/tabpy_server/handlers/base_handler.py index 5ba55546..8e330067 100644 --- a/tabpy/tabpy_server/handlers/base_handler.py +++ b/tabpy/tabpy_server/handlers/base_handler.py @@ -6,6 +6,7 @@ import tornado.web from tabpy.tabpy_server.app.SettingsParameters import SettingsParameters from tabpy.tabpy_server.handlers.util import hash_password +from tabpy.tabpy_server.handlers.util import AuthErrorStates import uuid @@ -132,16 +133,12 @@ def initialize(self, app): app.settings[SettingsParameters.LogRequestContext] ) self.logger.log(logging.DEBUG, "Checking if need to handle authentication") - self.not_authorized = not self.handle_authentication("v1") + self.auth_error = self.handle_authentication("v1") def error_out(self, code, log_message, info=None): self.set_status(code) self.write(json.dumps({"message": log_message, "info": info or {}})) - # We want to duplicate error message in console for - # loggers are misconfigured or causing the failure - # themselves - print(info) self.logger.log( logging.ERROR, 'Responding with status={}, message="{}", info="{}"'.format( @@ -364,7 +361,7 @@ def _validate_credentials(self, method) -> bool: ) return False - def handle_authentication(self, api_version) -> bool: + def handle_authentication(self, api_version): """ If authentication feature is configured checks provided credentials. @@ -376,27 +373,36 @@ def handle_authentication(self, api_version) -> bool: Returns ------- - bool - True if authentication is not required. - True if authentication is required and valid - credentials provided. - False otherwise. + String + None if authentication is not required and username and password are None. + None if authentication is required and valid credentials provided. + NotAuthorized if authenication is required and credentials are incorrect. + NotRequired if authentication is not required but credentials are provided. """ self.logger.log(logging.DEBUG, "Handling authentication") found, method = self._get_auth_method(api_version) if not found: - return False + return AuthErrorStates.NotAuthorized if method == "": - # Do not validate credentials - return True + if not self._get_basic_auth_credentials(): + self.logger.log(logging.DEBUG, + "authentication not required, username and password are none") + return AuthErrorStates.NONE + else: + self.logger.log(logging.DEBUG, + "authentication not required, username and password are not none") + return AuthErrorStates.NotRequired if not self._get_credentials(method): - return False + return AuthErrorStates.NotAuthorized - return self._validate_credentials(method) + if not self._validate_credentials(method): + return AuthErrorStates.NotAuthorized - def should_fail_with_not_authorized(self): + return AuthErrorStates.NONE + + def should_fail_with_auth_error(self): """ Checks if authentication is required: - if it is not returns false, None @@ -405,21 +411,35 @@ def should_fail_with_not_authorized(self): Returns ------- bool - False if authentication is not required or is - required and validation for credentials passes. - True if validation for credentials failed. + False if authentication is not required and username + and password is None or isrequired and validation + for credentials passes. + True if validation for credentials failed or + if authentication is not required and username and password + fields are not empty. """ - return self.not_authorized + return self.auth_error - def fail_with_not_authorized(self): + def fail_with_auth_error(self): """ - Prepares server 401 response. + Prepares server 401 response and server 400 response depending + on the value of the self.auth_error flag """ - self.logger.log(logging.ERROR, "Failing with 401 for unauthorized request") - self.set_status(401) - self.set_header("WWW-Authenticate", f'Basic realm="{self.tabpy_state.name}"') - self.error_out( - 401, - info="Unauthorized request.", - log_message="Invalid credentials provided.", - ) + if self.auth_error == AuthErrorStates.NotAuthorized: + self.logger.log(logging.ERROR, "Failing with 401 for unauthorized request") + self.set_status(401) + self.set_header("WWW-Authenticate", f'Basic realm="{self.tabpy_state.name}"') + self.error_out( + 401, + info="Unauthorized request.", + log_message="Invalid credentials provided.", + ) + else: + self.logger.log(logging.ERROR, "Failing with 400 for Bad Request") + self.set_status(400) + self.set_header("WWW-Authenticate", f'Basic realm="{self.tabpy_state.name}"') + self.error_out( + 400, + info="Bad request.", + log_message="Username or Password provided when authentication not available", + ) diff --git a/tabpy/tabpy_server/handlers/endpoint_handler.py b/tabpy/tabpy_server/handlers/endpoint_handler.py index 022d8e0b..583b1762 100644 --- a/tabpy/tabpy_server/handlers/endpoint_handler.py +++ b/tabpy/tabpy_server/handlers/endpoint_handler.py @@ -14,6 +14,7 @@ from tabpy.tabpy_server.handlers.base_handler import STAGING_THREAD from tabpy.tabpy_server.management.state import get_query_object_path from tabpy.tabpy_server.psws.callbacks import on_state_change +from tabpy.tabpy_server.handlers.util import AuthErrorStates from tornado import gen @@ -22,8 +23,8 @@ def initialize(self, app): super(EndpointHandler, self).initialize(app) def get(self, endpoint_name): - if self.should_fail_with_not_authorized(): - self.fail_with_not_authorized() + if self.should_fail_with_auth_error() != AuthErrorStates.NONE: + self.fail_with_auth_error() return self.logger.log(logging.DEBUG, f"Processing GET for /endpoints/{endpoint_name}") @@ -43,8 +44,8 @@ def get(self, endpoint_name): @gen.coroutine def put(self, name): - if self.should_fail_with_not_authorized(): - self.fail_with_not_authorized() + if self.should_fail_with_auth_error() != AuthErrorStates.NONE: + self.fail_with_auth_error() return self.logger.log(logging.DEBUG, f"Processing PUT for /endpoints/{name}") @@ -89,8 +90,8 @@ def put(self, name): @gen.coroutine def delete(self, name): - if self.should_fail_with_not_authorized(): - self.fail_with_not_authorized() + if self.should_fail_with_auth_error() != AuthErrorStates.NONE: + self.fail_with_auth_error() return self.logger.log(logging.DEBUG, f"Processing DELETE for /endpoints/{name}") diff --git a/tabpy/tabpy_server/handlers/endpoints_handler.py b/tabpy/tabpy_server/handlers/endpoints_handler.py index 66132dd2..bda8a16d 100644 --- a/tabpy/tabpy_server/handlers/endpoints_handler.py +++ b/tabpy/tabpy_server/handlers/endpoints_handler.py @@ -10,6 +10,7 @@ import logging from tabpy.tabpy_server.common.util import format_exception from tabpy.tabpy_server.handlers import ManagementHandler +from tabpy.tabpy_server.handlers.util import AuthErrorStates from tornado import gen @@ -18,8 +19,8 @@ def initialize(self, app): super(EndpointsHandler, self).initialize(app) def get(self): - if self.should_fail_with_not_authorized(): - self.fail_with_not_authorized() + if self.should_fail_with_auth_error() != AuthErrorStates.NONE: + self.fail_with_auth_error() return self._add_CORS_header() @@ -27,8 +28,8 @@ def get(self): @gen.coroutine def post(self): - if self.should_fail_with_not_authorized(): - self.fail_with_not_authorized() + if self.should_fail_with_auth_error() != AuthErrorStates.NONE: + self.fail_with_auth_error() return try: diff --git a/tabpy/tabpy_server/handlers/evaluation_plane_handler.py b/tabpy/tabpy_server/handlers/evaluation_plane_handler.py index 13ee1175..2ad55568 100644 --- a/tabpy/tabpy_server/handlers/evaluation_plane_handler.py +++ b/tabpy/tabpy_server/handlers/evaluation_plane_handler.py @@ -6,6 +6,7 @@ import requests from tornado import gen from datetime import timedelta +from tabpy.tabpy_server.handlers.util import AuthErrorStates class RestrictedTabPy: @@ -100,8 +101,8 @@ def _post_impl(self): @gen.coroutine def post(self): - if self.should_fail_with_not_authorized(): - self.fail_with_not_authorized() + if self.should_fail_with_auth_error() != AuthErrorStates.NONE: + self.fail_with_auth_error() return self._add_CORS_header() diff --git a/tabpy/tabpy_server/handlers/query_plane_handler.py b/tabpy/tabpy_server/handlers/query_plane_handler.py index aab42593..c66e9fa9 100644 --- a/tabpy/tabpy_server/handlers/query_plane_handler.py +++ b/tabpy/tabpy_server/handlers/query_plane_handler.py @@ -13,6 +13,7 @@ from tabpy.tabpy_server.common.util import format_exception import urllib from tornado import gen +from tabpy.tabpy_server.handlers.util import AuthErrorStates def _get_uuid(): @@ -67,8 +68,8 @@ def _query(self, po_name, data, uid, qry): # don't check API key (client does not send or receive data for OPTIONS, # it just allows the client to subsequently make a POST request) def options(self, pred_name): - if self.should_fail_with_not_authorized(): - self.fail_with_not_authorized() + if self.should_fail_with_auth_error() != AuthErrorStates.NONE: + self.fail_with_auth_error() return self.logger.log(logging.DEBUG, f"Processing OPTIONS for /query/{pred_name}") @@ -212,8 +213,8 @@ def _get_actual_model(self, endpoint_name): @gen.coroutine def get(self, endpoint_name): - if self.should_fail_with_not_authorized(): - self.fail_with_not_authorized() + if self.should_fail_with_auth_error() != AuthErrorStates.NONE: + self.fail_with_auth_error() return start = time.time() @@ -224,8 +225,8 @@ def get(self, endpoint_name): def post(self, endpoint_name): self.logger.log(logging.DEBUG, f"Processing POST for /query/{endpoint_name}...") - if self.should_fail_with_not_authorized(): - self.fail_with_not_authorized() + if self.should_fail_with_auth_error() != AuthErrorStates.NONE: + self.fail_with_auth_error() return start = time.time() diff --git a/tabpy/tabpy_server/handlers/service_info_handler.py b/tabpy/tabpy_server/handlers/service_info_handler.py index 51152a98..5ba1469d 100644 --- a/tabpy/tabpy_server/handlers/service_info_handler.py +++ b/tabpy/tabpy_server/handlers/service_info_handler.py @@ -1,15 +1,15 @@ import json from tabpy.tabpy_server.app.SettingsParameters import SettingsParameters from tabpy.tabpy_server.handlers import ManagementHandler - +from tabpy.tabpy_server.handlers.util import AuthErrorStates class ServiceInfoHandler(ManagementHandler): def initialize(self, app): super(ServiceInfoHandler, self).initialize(app) def get(self): - if self.should_fail_with_not_authorized(): - self.fail_with_not_authorized() + if self.should_fail_with_auth_error() != AuthErrorStates.NONE: + self.fail_with_auth_error() return self._add_CORS_header() diff --git a/tabpy/tabpy_server/handlers/status_handler.py b/tabpy/tabpy_server/handlers/status_handler.py index 2f743b3f..493ca036 100644 --- a/tabpy/tabpy_server/handlers/status_handler.py +++ b/tabpy/tabpy_server/handlers/status_handler.py @@ -1,15 +1,15 @@ import json import logging from tabpy.tabpy_server.handlers import BaseHandler - +from tabpy.tabpy_server.handlers.util import AuthErrorStates class StatusHandler(BaseHandler): def initialize(self, app): super(StatusHandler, self).initialize(app) def get(self): - if self.should_fail_with_not_authorized(): - self.fail_with_not_authorized() + if self.should_fail_with_auth_error() != AuthErrorStates.NONE: + self.fail_with_auth_error() return self._add_CORS_header() diff --git a/tabpy/tabpy_server/handlers/upload_destination_handler.py b/tabpy/tabpy_server/handlers/upload_destination_handler.py index 729aff3e..d6757dd1 100644 --- a/tabpy/tabpy_server/handlers/upload_destination_handler.py +++ b/tabpy/tabpy_server/handlers/upload_destination_handler.py @@ -1,6 +1,7 @@ from tabpy.tabpy_server.app.SettingsParameters import SettingsParameters from tabpy.tabpy_server.handlers import ManagementHandler import os +from tabpy.tabpy_server.handlers.util import AuthErrorStates _QUERY_OBJECT_STAGING_FOLDER = "staging" @@ -11,8 +12,8 @@ def initialize(self, app): super(UploadDestinationHandler, self).initialize(app) def get(self): - if self.should_fail_with_not_authorized(): - self.fail_with_not_authorized() + if self.should_fail_with_auth_error() != AuthErrorStates.NONE: + self.fail_with_auth_error() return path = self.settings[SettingsParameters.StateFilePath] diff --git a/tabpy/tabpy_server/handlers/util.py b/tabpy/tabpy_server/handlers/util.py index 14e029c6..ae9d7387 100644 --- a/tabpy/tabpy_server/handlers/util.py +++ b/tabpy/tabpy_server/handlers/util.py @@ -1,5 +1,12 @@ import binascii from hashlib import pbkdf2_hmac +from enum import Enum, auto + + +class AuthErrorStates(Enum): + NONE = auto() + NotAuthorized = auto() + NotRequired = auto() def hash_password(username, pwd): diff --git a/tests/integration/integ_test_base.py b/tests/integration/integ_test_base.py index 9453a1f6..43ad9efe 100755 --- a/tests/integration/integ_test_base.py +++ b/tests/integration/integ_test_base.py @@ -169,7 +169,7 @@ 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_QUERY_OBJECT_PATH = ./query_objects\n" + f"TABPY_QUERY_OBJECT_PATH = {self.tmp_dir}/query_objects\n" f"TABPY_PORT = {self._get_port()}\n" f"TABPY_STATE_PATH = {self.tmp_dir}\n" ) diff --git a/tests/unit/server_tests/test_endpoint_handler.py b/tests/unit/server_tests/test_endpoint_handler.py index a9393c42..31b778ed 100755 --- a/tests/unit/server_tests/test_endpoint_handler.py +++ b/tests/unit/server_tests/test_endpoint_handler.py @@ -111,3 +111,55 @@ def test_valid_creds_unknown_endpoint_fails(self): }, ) self.assertEqual(404, response.code) + + +class TestEndpointHandlerWithoutAuth(AsyncHTTPTestCase): + @classmethod + def setUpClass(cls): + _init_asyncio_patch() + prefix = "__TestEndpointHandlerWithoutAuth_" + + # create state.ini dir and file + cls.state_dir = tempfile.mkdtemp(prefix=prefix) + cls.state_file = open(os.path.join(cls.state_dir, "state.ini"), "w+") + cls.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" + ) + cls.state_file.close() + + @classmethod + def tearDownClass(cls): + os.remove(cls.state_file.name) + os.rmdir(cls.state_dir) + + def get_app(self): + self.app = TabPyApp(None) + return self.app._create_tornado_web_app() + + def test_creds_no_auth_fails(self): + response = self.fetch( + "/endpoints/", + method="GET", + headers={ + "Authorization": "Basic {}".format( + base64.b64encode("username:password".encode("utf-8")).decode( + "utf-8" + ) + ) + }, + ) + self.assertEqual(400, response.code) + \ No newline at end of file diff --git a/tests/unit/server_tests/test_endpoints_handler.py b/tests/unit/server_tests/test_endpoints_handler.py index 6ae00fc7..5255b2b4 100755 --- a/tests/unit/server_tests/test_endpoints_handler.py +++ b/tests/unit/server_tests/test_endpoints_handler.py @@ -94,3 +94,53 @@ def test_valid_creds_pass(self): }, ) self.assertEqual(200, response.code) + + +class TestEndpointsHandlerWithoutAuth(AsyncHTTPTestCase): + @classmethod + def setUpClass(cls): + prefix = "__TestEndpointsHandlerWithoutAuth_" + + # create state.ini dir and file + cls.state_dir = tempfile.mkdtemp(prefix=prefix) + cls.state_file = open(os.path.join(cls.state_dir, "state.ini"), "w+") + cls.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" + ) + cls.state_file.close() + + @classmethod + def tearDownClass(cls): + os.remove(cls.state_file.name) + os.rmdir(cls.state_dir) + + def get_app(self): + self.app = TabPyApp(None) + return self.app._create_tornado_web_app() + + def test_creds_no_auth_fails(self): + response = self.fetch( + "/endpoints", + method="GET", + headers={ + "Authorization": "Basic {}".format( + base64.b64encode("username:password".encode("utf-8")).decode( + "utf-8" + ) + ) + }, + ) + self.assertEqual(400, response.code) diff --git a/tests/unit/server_tests/test_evaluation_plane_handler.py b/tests/unit/server_tests/test_evaluation_plane_handler.py index 49b67dfb..52b0d98a 100755 --- a/tests/unit/server_tests/test_evaluation_plane_handler.py +++ b/tests/unit/server_tests/test_evaluation_plane_handler.py @@ -203,3 +203,86 @@ def test_script_returns_none(self): }) self.assertEqual(200, response.code) self.assertEqual(b'null', response.body) + + +class TestEvaluationPlainHandlerWithoutAuth(AsyncHTTPTestCase): + @classmethod + def setUpClass(cls): + prefix = "__TestEvaluationPlainHandlerWithoutAuth_" + + # create state.ini dir and file + cls.state_dir = tempfile.mkdtemp(prefix=prefix) + cls.state_file = open(os.path.join(cls.state_dir, "state.ini"), "w+") + cls.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" + ) + cls.state_file.close() + + cls.script = ( + '{"data":{"_arg1":[2,3],"_arg2":[3,-1]},' + '"script":"res=[]\\nfor i in range(len(_arg1)):\\n ' + 'res.append(_arg1[i] * _arg2[i])\\nreturn res"}' + ) + + cls.script_not_present = ( + '{"data":{"_arg1":[2,3],"_arg2":[3,-1]},' + '"":"res=[]\\nfor i in range(len(_arg1)):\\n ' + 'res.append(_arg1[i] * _arg2[i])\\nreturn res"}' + ) + + cls.args_not_present = ( + '{"script":"res=[]\\nfor i in range(len(_arg1)):\\n ' + 'res.append(_arg1[i] * _arg2[i])\\nreturn res"}' + ) + + cls.args_not_sequential = ( + '{"data":{"_arg1":[2,3],"_arg3":[3,-1]},' + '"script":"res=[]\\nfor i in range(len(_arg1)):\\n ' + 'res.append(_arg1[i] * _arg3[i])\\nreturn res"}' + ) + + cls.nan_coverts_to_null =\ + '{"data":{"_arg1":[2,3],"_arg2":[3,-1]},'\ + '"script":"return [float(1), float(\\"NaN\\"), float(2)]"}' + + cls.script_returns_none = ( + '{"data":{"_arg1":[2,3],"_arg2":[3,-1]},' + '"script":"return None"}' + ) + + @classmethod + def tearDownClass(cls): + os.remove(cls.state_file.name) + os.rmdir(cls.state_dir) + + def get_app(self): + self.app = TabPyApp(None) + return self.app._create_tornado_web_app() + + def test_creds_no_auth_fails(self): + response = self.fetch( + "/evaluate", + method="POST", + body=self.script, + headers={ + "Authorization": "Basic {}".format( + base64.b64encode("username:password".encode("utf-8")).decode( + "utf-8" + ) + ) + }, + ) + self.assertEqual(400, response.code) diff --git a/tests/unit/server_tests/test_service_info_handler.py b/tests/unit/server_tests/test_service_info_handler.py index 767a50c0..8751ee8a 100644 --- a/tests/unit/server_tests/test_service_info_handler.py +++ b/tests/unit/server_tests/test_service_info_handler.py @@ -137,20 +137,4 @@ def test_given_server_with_no_auth_and_password_expect_correct_info_response(sel } response = self.fetch("/info", headers=header) - self.assertEqual(response.code, 200) - actual_response = json.loads(response.body) - expected_response = _create_expected_info_response( - self.app.settings, self.app.tabpy_state - ) - - self.assertDictEqual(actual_response, expected_response) - self.assertTrue("versions" in actual_response) - versions = actual_response["versions"] - self.assertTrue("v1" in versions) - v1 = versions["v1"] - self.assertTrue("features" in v1) - features = v1["features"] - self.assertDictEqual( - {}, - features, - ) + self.assertEqual(response.code, 400)