diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 76f9875..79495f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: run: | python -m pip install --upgrade pip pip install poetry - poetry install + poetry install --no-root - name: Linting run: | poetry run flake8 @@ -48,7 +48,7 @@ jobs: run: | python -m pip install --upgrade pip pip install poetry - poetry install + poetry install --no-root - name: Generate coverage report run: | poetry run pytest diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c51092a..1742522 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -17,7 +17,7 @@ jobs: run: | python -m pip install --upgrade pip pip install poetry - poetry install + poetry install --no-root - name: Linting run: | poetry run flake8 diff --git a/connect/eaas/core/logging.py b/connect/eaas/core/logging.py index 7514d7d..90bbb48 100644 --- a/connect/eaas/core/logging.py +++ b/connect/eaas/core/logging.py @@ -2,6 +2,8 @@ from logzio.handler import LogzioHandler +from connect.eaas.core.utils import obfuscate_header + class ExtensionLogHandler(LogzioHandler): def __init__(self, *args, **kwargs): @@ -19,16 +21,7 @@ def __init__(self, logger): self.logger = logger def obfuscate(self, key, value): - if key in ('authorization', 'authentication'): - if value.startswith('ApiKey '): - return value.split(':')[0] + ':' + '*' * 10 - else: - return '*' * 20 - if key in ('cookie', 'set-cookie') and 'api_key="' in value: - start_idx = value.index('api_key="') + len('api_key="') - end_idx = value.index('"', start_idx) - return f'{value[0:start_idx + 2]}******{value[end_idx - 2:]}' - return value + return obfuscate_header(key, value) def log_request(self, method, url, kwargs): other_args = {k: v for k, v in kwargs.items() if k not in ('headers', 'json', 'params')} diff --git a/connect/eaas/core/proto.py b/connect/eaas/core/proto.py index b2b87e4..270dfea 100644 --- a/connect/eaas/core/proto.py +++ b/connect/eaas/core/proto.py @@ -3,6 +3,8 @@ from pydantic import BaseModel as PydanticBaseModel, Field from pydantic.utils import DUNDER_ATTRIBUTES +from connect.eaas.core.utils import obfuscate_header + class BaseModel(PydanticBaseModel): @@ -10,13 +12,19 @@ def get_sensitive_fields(self): return [] # pragma: no cover def __obfuscate_args__(self, k, v): - if isinstance(v, str) and v and k in self.get_sensitive_fields(): + if k not in self.get_sensitive_fields() or not v: + return k, v + + if hasattr(self, f'obfuscate_{k}'): + return getattr(self, f'obfuscate_{k}')(k, v) + + if isinstance(v, str): return k, f'{v[0:2]}******{v[-2:]}' - if isinstance(v, (list, dict)) and v and k in self.get_sensitive_fields(): + if isinstance(v, (list, dict)): return k, '******' - return k, v + return k, v # pragma: no cover def __repr_args__(self): return [ @@ -103,7 +111,7 @@ class EventDefinition(BaseModel): class SetupResponse(BaseModel): - variables: Optional[dict] + variables: Optional[list] # delete after stop using version 1 environment_type: Optional[str] logging: Optional[Logging] @@ -113,6 +121,19 @@ class SetupResponse(BaseModel): def get_sensitive_fields(self): return ['variables'] + def obfuscate_variables(self, k, v): + return k, [ + { + 'name': item['name'], + 'value': ( + f'{item["value"][0:2]}******{item["value"][-2:]}' + if item['secure'] else item['value'] + ), + 'secure': item['secure'], + } + for item in v + ] + class Schedulable(BaseModel): method: str @@ -156,6 +177,9 @@ class HttpRequest(BaseModel): def get_sensitive_fields(self): return ['headers'] + def obfuscate_headers(self, k, v): + return k, {key: obfuscate_header(key.lower(), value) for key, value in v.items()} + class WebTaskOptions(BaseModel): correlation_id: str @@ -218,7 +242,7 @@ def serialize(self, protocol_version=2): return { 'message_type': MessageType.CONFIGURATION, 'data': { - 'configuration': self.data.variables, + 'configuration': {item['name']: item['value'] for item in self.data.variables}, 'environment_type': self.data.environment_type, 'logging_api_key': self.data.logging.logging_api_key, 'log_level': self.data.logging.log_level, @@ -286,7 +310,10 @@ def deserialize(cls, raw): message_type=MessageType.SETUP_RESPONSE, data=SetupResponse( environment_type=raw_data.get('environment_type'), - variables=raw_data.get('configuration'), + variables=[ + {'name': name, 'value': value, 'secure': False} + for name, value in raw_data.get('configuration', {}).items() + ], logging=Logging( logging_api_key=raw_data.get('logging_api_key'), log_level=raw_data.get('log_level'), diff --git a/connect/eaas/core/testing.py b/connect/eaas/core/testing.py deleted file mode 100644 index 298aaa5..0000000 --- a/connect/eaas/core/testing.py +++ /dev/null @@ -1,28 +0,0 @@ -from fastapi import FastAPI -from fastapi.staticfiles import StaticFiles -from starlette.testclient import TestClient - - -class WebAppTestClient(TestClient): - - def __init__(self, webapp, default_headers=None): - self._webapp_class = webapp - self.app = self.get_application() - - super().__init__(app=self.app) - - if default_headers: - self.headers = default_headers - - def get_application(self): - app = FastAPI() - - auth_router, no_auth_router = self._webapp_class.get_routers() - app.include_router(auth_router, prefix='/api') - app.include_router(no_auth_router, prefix='/guest') - - static_root = self._webapp_class.get_static_root() - if static_root: - app.mount('/static', StaticFiles(directory=static_root), name='static') - - return app diff --git a/connect/eaas/core/testing/__init__.py b/connect/eaas/core/testing/__init__.py new file mode 100644 index 0000000..445b348 --- /dev/null +++ b/connect/eaas/core/testing/__init__.py @@ -0,0 +1 @@ +from connect.eaas.core.testing.testclient import WebAppTestClient # noqa diff --git a/connect/eaas/core/testing/fixtures.py b/connect/eaas/core/testing/fixtures.py new file mode 100644 index 0000000..4a721dc --- /dev/null +++ b/connect/eaas/core/testing/fixtures.py @@ -0,0 +1,16 @@ +import pytest + +from connect.eaas.core.testing import WebAppTestClient + + +@pytest.fixture +def test_client_factory(): + """ + This fixture allows to instantiate a WebAppTestClient + given a webapp class. + """ + + def _get_client(webapp): + return WebAppTestClient(webapp) + + return _get_client diff --git a/connect/eaas/core/testing/testclient.py b/connect/eaas/core/testing/testclient.py new file mode 100644 index 0000000..ac196b2 --- /dev/null +++ b/connect/eaas/core/testing/testclient.py @@ -0,0 +1,152 @@ +import inspect +import json +from urllib.parse import urlparse + +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles +from starlette.routing import Match +from starlette.testclient import TestClient + +from connect.client.testing import AsyncConnectClientMocker, ConnectClientMocker +from connect.eaas.core.inject.models import Context + + +class WebAppTestClient(TestClient): + + def __init__(self, webapp): + self._webapp_class = webapp + self._app = self._get_application() + + super().__init__(app=self._app, base_url='https://localhost/public/v1') + + self.headers = { + 'X-Connect-Api-Gateway-Url': self.base_url, + 'X-Connect-User-Agent': 'eaas-test-client', + 'X-Connect-Installation-Api-Key': 'ApiKey XXXX', + } + + def request( + self, + method, + url, + params=None, + data=None, + headers=None, + cookies=None, + files=None, + auth=None, + timeout=None, + allow_redirects=True, + proxies=None, + hooks=None, + stream=None, + verify=None, + cert=None, + json=None, + context=None, + installation=None, + config=None, + log_level=None, + ): + headers = self._populate_internal_headers( + headers or {}, + context=context, + installation=installation, + config=config, + log_level=log_level, + ) + mocker = self._get_client_mocker(method, url) + if installation and mocker: + with mocker(self.base_url) as mocker: + mocker.ns('devops').collection('installations').resource( + installation['id'], + ).get(return_value=installation) + return super().request( + method, + url, + params=params, + data=data, + headers=headers, + cookies=cookies, + files=files, + auth=auth, + timeout=timeout, + allow_redirects=allow_redirects, + proxies=proxies, + hooks=hooks, + stream=stream, + verify=verify, + cert=cert, + json=json, + ) + return super().request( + method, + url, + params=params, + data=data, + headers=headers, + cookies=cookies, + files=files, + auth=auth, + timeout=timeout, + allow_redirects=allow_redirects, + proxies=proxies, + hooks=hooks, + stream=stream, + verify=verify, + cert=cert, + json=json, + ) + + def _get_client_mocker(self, method, url): + path = urlparse(url).path + for route in self.app.router.routes: + match, child_scope = route.matches({'type': 'http', 'method': method, 'path': path}) + if match == Match.FULL: + if inspect.iscoroutinefunction(child_scope['endpoint']): + return AsyncConnectClientMocker + return ConnectClientMocker + + def _generate_call_context(self, installation): + return Context(**{ + 'installation_id': installation['id'] if installation else 'EIN-000', + 'user_id': 'UR-000', + 'account_id': 'VA-000', + 'account_role': 'vendor', + 'call_source': 'ui', + 'call_type': 'user', + }) + + def _populate_internal_headers( + self, + headers, + config=None, + installation=None, + context=None, + log_level=None, + ): + headers['X-Connect-Logging-Level'] = log_level or 'INFO' + if config: + headers['X-Connect-Config'] = json.dumps(config) + + context: Context = context or self._generate_call_context(installation) + headers['X-Connect-Installation-id'] = context.installation_id + headers['X-Connect-User-Id'] = context.user_id + headers['X-Connect-Account-Id'] = context.account_id + headers['X-Connect-Account-Role'] = context.account_role + headers['X-Connect-Call-Source'] = context.call_source + headers['X-Connect-Call-Type'] = context.call_type + return headers + + def _get_application(self): + app = FastAPI() + + auth_router, no_auth_router = self._webapp_class.get_routers() + app.include_router(auth_router, prefix='/api') + app.include_router(no_auth_router, prefix='/guest') + + static_root = self._webapp_class.get_static_root() + if static_root: + app.mount('/static', StaticFiles(directory=static_root), name='static') + + return app diff --git a/connect/eaas/core/utils.py b/connect/eaas/core/utils.py new file mode 100644 index 0000000..87220a8 --- /dev/null +++ b/connect/eaas/core/utils.py @@ -0,0 +1,11 @@ +def obfuscate_header(key, value): + if key in ('authorization', 'authentication'): + if value.startswith('ApiKey '): + return value.split(':')[0] + ':' + '*' * 10 + else: + return '*' * 20 + if key in ('cookie', 'set-cookie') and 'api_key="' in value: + start_idx = value.index('api_key="') + len('api_key="') + end_idx = value.index('"', start_idx) + return f'{value[0:start_idx + 2]}******{value[end_idx - 2:]}' + return value diff --git a/connect/eaas/core/validation/validators/base.py b/connect/eaas/core/validation/validators/base.py index 3d021fc..fb91dab 100644 --- a/connect/eaas/core/validation/validators/base.py +++ b/connect/eaas/core/validation/validators/base.py @@ -86,7 +86,7 @@ def validate_pyproject_toml(context): # noqa: CCR001 return ValidationResult(items=messages, must_exit=True) sys.path.append(os.path.join(os.getcwd(), project_dir)) - possible_extensions = ['extension', 'webapp', 'anvil'] + possible_extensions = ['extension', 'webapp', 'anvilapp', 'eventsapp'] extensions = {} for extension_type in possible_extensions: if extension_type in extension_dict.keys(): @@ -108,20 +108,33 @@ def validate_pyproject_toml(context): # noqa: CCR001 extensions[extension_type] = getattr(extension_module, class_name) + if 'extension' in extension_dict: + messages.append( + ValidationItem( + level='WARNING', + message=( + 'Declaring an events application using the *extension* entrypoint name is ' + 'deprecated in favor of *eventsapp*.' + ), + file=descriptor_file, + ), + ) + if not extensions: messages.append( ValidationItem( level='ERROR', message=( - 'Invalid extension declaration in *[tool.poetry.plugins."connect.eaas.ext"]*: ' - 'The extension must be declared as: *"extension" = ' + 'Invalid application declaration in ' + '*[tool.poetry.plugins."connect.eaas.ext"]*: ' + 'The application must be declared as: *"eventsapp" = ' '"your_package.extension:YourApplication"* ' 'for Fulfillment automation or Hub integration. ' 'For Multi account installation must be ' - 'declared at least one the following: *"extension" = ' + 'declared at least one the following: *"eventsapp" = ' '"your_package.events:YourEventsApplication"*, ' '*"webapp" = "your_package.webapp:YourWebApplication"*, ' - '*"anvil" = "your_package.anvil:YourAnvilApplication"*.' + '*"anvilapp" = "your_package.anvil:YourAnvilApplication"*.' ), file=descriptor_file, ), diff --git a/pyproject.toml b/pyproject.toml index 7d31598..5dec821 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,11 +53,14 @@ pytest-asyncio = "^0.18.3" Faker = "^14.2.0" [build-system] -requires = ["poetry-core>=1.0.0", "setuptools"] +requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" +[tool.poetry.plugins.pytest11] +"pytest_eaas_core" = "connect.eaas.core.testing.fixtures" + [tool.pytest.ini_options] -testpaths = "tests" +testpaths = ["tests"] log_cli = true addopts = "--cov=connect.eaas.core --cov-report=term-missing:skip-covered --cov-report=html --cov-report=xml" asyncio_mode = "strict" diff --git a/tests/conftest.py b/tests/conftest.py index 06507aa..f9a80d3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,15 +1,13 @@ -import inspect -import os - import pytest import responses as sentry_responses -from fastapi import Depends, Request +from fastapi import Depends from fastapi.routing import APIRouter -from connect.client import ConnectClient -from connect.eaas.core.decorators import guest, web_app +from connect.eaas.core.decorators import web_app from connect.eaas.core.extension import WebApplicationBase -from connect.eaas.core.inject.synchronous import get_installation, get_installation_client +from connect.eaas.core.inject import asynchronous, common, synchronous +from connect.eaas.core.inject.models import Context +from connect.eaas.core.testing.fixtures import test_client_factory # noqa @pytest.fixture @@ -25,48 +23,26 @@ def webapp_mock(mocker): mocker.patch('connect.eaas.core.extension.router', router) @web_app(router) - class MyWebExtension(WebApplicationBase): + class MyWebApplication(WebApplicationBase): - @router.get('/settings') - def retrieve_settings(self, installation: dict = Depends(get_installation)) -> dict: + @router.get('/sync/installation') + def sync_installation( + self, installation: dict = Depends(synchronous.get_installation), + ) -> dict: return installation - @router.delete('/settings/{item_id}') - def delete_settings( - self, - item_id, - ) -> str: - return item_id - - @router.post('/settings') - async def update_settings( - self, - request: Request, - installation_client: ConnectClient = Depends(get_installation_client), - installation: dict = Depends(get_installation), - ): - settings = await request.json() - - installation_client('devops').installations[installation['id']].update( - {'settings': settings}, - ) - return installation_client('devops').installations[installation['id']].get() + @router.get('/async/installation') + async def async_installation( + self, installation: dict = Depends(asynchronous.get_installation), + ) -> dict: + return installation - @guest() - @router.get('/whoami') - def whoami(self) -> dict: - return {'test': 'client'} + @router.get('/config') + def config(self, config: dict = Depends(common.get_config)) -> dict: + return config - @classmethod - def get_static_root(cls): - static_root = os.path.abspath( - os.path.join( - os.path.dirname(inspect.getfile(cls)), - 'static_root', - ), - ) - if os.path.exists(static_root) and os.path.isdir(static_root): - return static_root - return None + @router.get('/context') + def context(self, context: Context = Depends(common.get_call_context)): + return context - yield MyWebExtension + return MyWebApplication diff --git a/tests/connect/eaas/core/test_proto.py b/tests/connect/eaas/core/test_proto.py index 6963639..997c5c3 100644 --- a/tests/connect/eaas/core/test_proto.py +++ b/tests/connect/eaas/core/test_proto.py @@ -138,7 +138,7 @@ } SETUP_RESPONSE_DATA = { - 'variables': {'conf1': 'val1'}, + 'variables': [{'name': 'conf1', 'value': 'val1', 'secure': False}], 'environment_type': 'environ-type', 'logging': { 'logging_api_key': 'logging-token', @@ -467,10 +467,34 @@ def test_obfuscate_logging(): def test_obfuscate_setup_response(): - setup_response = SetupResponse(variables={'VAR1': 'VAL1'}) + setup_response = SetupResponse( + variables=[ + { + 'name': 'SECURE_VAR', + 'value': 'abcdefgh', + 'secure': True, + }, + { + 'name': 'UNSECURE_VAR', + 'value': 'qwerty', + 'secure': False, + }, + ], + ) assert next(filter( lambda x: x[0] == 'variables', setup_response.__repr_args__(), - ))[1] == '******' + ))[1] == [ + { + 'name': 'SECURE_VAR', + 'value': 'ab******gh', + 'secure': True, + }, + { + 'name': 'UNSECURE_VAR', + 'value': 'qwerty', + 'secure': False, + }, + ] def test_obfuscate_setup_request(): @@ -482,9 +506,11 @@ def test_obfuscate_http_request(): http_request = HttpRequest( method='method', url='url', - headers={'Authorization': 'My Api Key'}, + headers={'Authorization': 'ApiKey SU-0000:abcdef'}, ) - assert next(filter(lambda x: x[0] == 'headers', http_request.__repr_args__()))[1] == '******' + assert next(filter(lambda x: x[0] == 'headers', http_request.__repr_args__()))[1] == { + 'Authorization': 'ApiKey SU-0000:**********', + } def test_obfuscate_webtask_options(): diff --git a/tests/connect/eaas/core/test_webapp.py b/tests/connect/eaas/core/test_webapp.py deleted file mode 100644 index eda8e40..0000000 --- a/tests/connect/eaas/core/test_webapp.py +++ /dev/null @@ -1,123 +0,0 @@ -import json - -from connect.eaas.core.testing import WebAppTestClient - - -def test_get_settings(webapp_mock, client_mocker_factory): - mocker = client_mocker_factory('https://localhost/public/v1') - mocker('devops').installations['installation_id'].get(return_value={'id': 'EIN-000-000'}) - - client = WebAppTestClient( - webapp_mock, - default_headers={ - 'X-Connect-Api-Gateway-Url': 'https://localhost/public/v1', - 'X-Connect-User-Agent': 'user-agent', - }, - ) - - response = client.get( - '/api/settings', - headers={ - 'X-Connect-Installation-Api-Key': 'installation_api_key', - 'X-Connect-Installation-Id': 'installation_id', - 'X-Connect-Account-Id': 'account_id', - 'X-Connect-Account-Role': 'account_role', - 'X-Connect-User-Id': 'user_id', - 'X-Connect-Call-Source': 'ui', - 'X-Connect-Call-Type': 'user', - - }, - ) - data = response.json() - - assert response.status_code == 200 - assert data == {'id': 'EIN-000-000'} - - -def test_delete_settings(webapp_mock): - client = WebAppTestClient(webapp_mock) - response = client.delete( - '/api/settings/123', - headers={ - 'X-Connect-Installation-Api-Key': 'installation_api_key', - 'X-Connect-Installation-Id': 'installation_id', - 'X-Connect-Api-Gateway-Url': 'https://localhost/public/v1', - 'X-Connect-User-Agent': 'user-agent', - 'X-Connect-Account-Id': 'account_id', - 'X-Connect-Account-Role': 'account_role', - 'X-Connect-User-Id': 'user_id', - 'X-Connect-Call-Source': 'ui', - 'X-Connect-Call-Type': 'user', - }, - ) - data = response.json() - - assert response.status_code == 200 - assert data == '123' - - -def test_update_settings(webapp_mock, client_mocker_factory): - mocker = client_mocker_factory('https://localhost/public/v1') - mocker('devops').installations['installation_id'].get(return_value={'id': 'EIN-000-000'}) - mocker('devops').installations['EIN-000-000'].update(return_value={'id': 'EIN-000-000'}) - mocker('devops').installations['EIN-000-000'].get( - return_value={'id': 'EIN-000-000', 'settings': {'attr': 'new_value'}}, - ) - - client = WebAppTestClient( - webapp_mock, - default_headers={ - 'X-Connect-Api-Gateway-Url': 'https://localhost/public/v1', - 'X-Connect-User-Agent': 'user-agent', - }, - ) - response = client.post( - '/api/settings', - headers={ - 'X-Connect-Installation-Api-Key': 'installation_api_key', - 'X-Connect-Installation-Id': 'installation_id', - 'X-Connect-Account-Id': 'account_id', - 'X-Connect-Account-Role': 'account_role', - 'X-Connect-User-Id': 'user_id', - 'X-Connect-Call-Source': 'ui', - 'X-Connect-Call-Type': 'user', - }, - data=json.dumps({'attr': 'new_value'}).encode('utf-8'), - ) - data = response.json() - - assert response.status_code == 200 - assert data == {'id': 'EIN-000-000', 'settings': {'attr': 'new_value'}} - - -def test_whoami(webapp_mock): - client = WebAppTestClient( - webapp_mock, - default_headers={ - 'X-Connect-Api-Gateway-Url': 'https://localhost/public/v1', - 'X-Connect-User-Agent': 'user-agent', - }, - ) - - response = client.get('/guest/whoami') - assert response.json() == {'test': 'client'} - - -def test_static_files(webapp_mock): - client = WebAppTestClient( - webapp_mock, - default_headers={ - 'X-Connect-Api-Gateway-Url': 'https://localhost/public/v1', - 'X-Connect-User-Agent': 'user-agent', - }, - ) - - response = client.get( - '/static/example.html', - headers={ - 'X-Connect-Installation-Api-Key': 'installation_api_key', - 'X-Connect-Installation-Id': 'installation_id', - }, - ) - - assert response.text == 'Hello world!' diff --git a/tests/connect/eaas/core/testing/__init__.py b/tests/connect/eaas/core/testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/connect/eaas/core/testing/test_fixtures.py b/tests/connect/eaas/core/testing/test_fixtures.py new file mode 100644 index 0000000..42e20dc --- /dev/null +++ b/tests/connect/eaas/core/testing/test_fixtures.py @@ -0,0 +1,7 @@ +from connect.eaas.core.testing import WebAppTestClient + + +def test_test_client_factory(test_client_factory, webapp_mock): + client = test_client_factory(webapp_mock) + assert isinstance(client, WebAppTestClient) + assert client._webapp_class == webapp_mock diff --git a/tests/connect/eaas/core/testing/test_testclient.py b/tests/connect/eaas/core/testing/test_testclient.py new file mode 100644 index 0000000..680a5f4 --- /dev/null +++ b/tests/connect/eaas/core/testing/test_testclient.py @@ -0,0 +1,107 @@ +import pytest + +from connect.eaas.core.inject.models import Context +from connect.eaas.core.testing import WebAppTestClient + + +@pytest.mark.parametrize( + 'url', + ( + '/api/sync/installation', + '/api/async/installation', + '/api/sync/installation?param=param', + '/api/async/installation?param=param', + ), +) +def test_get_installation(webapp_mock, url): + client = WebAppTestClient(webapp_mock) + + installation = { + 'id': 'EIN-012', + 'settings': {'test': 'settings'}, + } + + resp = client.get(url, installation=installation) + + assert resp.status_code == 200 + assert resp.json() == installation + + +def test_get_config_default(webapp_mock): + client = WebAppTestClient(webapp_mock) + resp = client.get('/api/config') + + assert resp.status_code == 200 + assert resp.json() == {} + + +def test_get_config_custom(webapp_mock): + client = WebAppTestClient(webapp_mock) + config = {'VAR1': 'value1'} + + resp = client.get('/api/config', config=config) + + assert resp.status_code == 200 + assert resp.json() == config + + +def test_get_call_context_default(webapp_mock): + client = WebAppTestClient(webapp_mock) + + resp = client.get('/api/context') + + assert resp.status_code == 200 + assert resp.json() == { + 'installation_id': 'EIN-000', + 'user_id': 'UR-000', + 'account_id': 'VA-000', + 'account_role': 'vendor', + 'call_source': 'ui', + 'call_type': 'user', + } + + +def test_get_call_context_custom(webapp_mock): + client = WebAppTestClient(webapp_mock) + + ctx = { + 'installation_id': 'EIN-111', + 'user_id': 'UR-222', + 'account_id': 'PA-333', + 'account_role': 'distributor', + 'call_source': 'api', + 'call_type': 'admin', + } + + resp = client.get('/api/context', context=Context(**ctx)) + + assert resp.status_code == 200 + assert resp.json() == ctx + + +def test_not_found(webapp_mock): + client = WebAppTestClient(webapp_mock) + installation = { + 'id': 'EIN-012', + 'settings': {'test': 'settings'}, + } + resp = client.get('/not/fonund', installation=installation) + + assert resp.status_code == 404 + + +def test_method_not_allowed(webapp_mock): + client = WebAppTestClient(webapp_mock) + installation = { + 'id': 'EIN-012', + 'settings': {'test': 'settings'}, + } + resp = client.post('/api/async/installation', installation=installation) + + assert resp.status_code == 405 + + +def test_static(webapp_mock): + client = WebAppTestClient(webapp_mock) + resp = client.get('/static/example.html') + assert resp.status_code == 200 diff --git a/tests/connect/eaas/core/validation/validators/test_base.py b/tests/connect/eaas/core/validation/validators/test_base.py index 447c40b..f6b60e8 100644 --- a/tests/connect/eaas/core/validation/validators/test_base.py +++ b/tests/connect/eaas/core/validation/validators/test_base.py @@ -63,7 +63,7 @@ def test_validate_pyproject_toml_depends_on_runner(mocker): }, 'plugins': { 'connect.eaas.ext': { - 'extension': 'root_pkg.extension:MyExtension', + 'eventsapp': 'root_pkg.extension:MyExtension', }, }, }, @@ -102,7 +102,7 @@ def test_validate_pyproject_toml_missed_eaas_core_dependency(mocker): 'dependencies': {}, 'plugins': { 'connect.eaas.ext': { - 'extension': 'root_pkg.extension:MyExtension', + 'eventsapp': 'root_pkg.extension:MyExtension', }, }, }, @@ -198,11 +198,11 @@ def test_validate_pyproject_toml_invalid_extension_declaration(mocker): item = result.items[0] assert isinstance(item, ValidationItem) assert item.level == 'ERROR' - assert 'Invalid extension declaration in' in item.message + assert 'Invalid application declaration in' in item.message assert item.file == 'fake_dir/pyproject.toml' -def test_validate_pyproject_toml_import_error(mocker): +def test_validate_pyproject_toml_deprecated_extension_declaration(mocker): mocker.patch( 'connect.eaas.core.validation.validators.base.os.path.isfile', return_value=True, @@ -224,6 +224,49 @@ def test_validate_pyproject_toml_import_error(mocker): }, }, ) + mocker.patch('connect.eaas.core.validation.validators.base.importlib.import_module') + mocker.patch( + 'connect.eaas.core.validation.validators.base.inspect.getmembers', + return_value=[], + ) + + result = validate_pyproject_toml({'project_dir': 'fake_dir'}) + + assert isinstance(result, ValidationResult) + assert result.must_exit is False + assert len(result.items) == 1 + item = result.items[0] + assert isinstance(item, ValidationItem) + assert item.level == 'WARNING' + assert item.message == ( + 'Declaring an events application using the *extension* entrypoint name is ' + 'deprecated in favor of *eventsapp*.' + ) + assert item.file == 'fake_dir/pyproject.toml' + + +def test_validate_pyproject_toml_import_error(mocker): + mocker.patch( + 'connect.eaas.core.validation.validators.base.os.path.isfile', + return_value=True, + ) + mocker.patch( + 'connect.eaas.core.validation.validators.base.toml.load', + return_value={ + 'tool': { + 'poetry': { + 'dependencies': { + 'connect-eaas-core': '1.0.0', + }, + 'plugins': { + 'connect.eaas.ext': { + 'eventsapp': 'root_pkg.extension:MyExtension', + }, + }, + }, + }, + }, + ) mocker.patch( 'connect.eaas.core.validation.validators.base.importlib.import_module', side_effect=ImportError(), diff --git a/tests/static_root/example.html b/tests/static/example.html similarity index 100% rename from tests/static_root/example.html rename to tests/static/example.html