From d02b1a79aa902429ee0d538127e301fda5a033e9 Mon Sep 17 00:00:00 2001 From: Alex Zywicki Date: Wed, 10 Nov 2021 16:29:17 -0600 Subject: [PATCH] Adding basic integration tests --- asynction/default_handlers.py | 7 ++ asynction/server.py | 9 ++ tests/fixtures/__init__.py | 4 + tests/fixtures/handlers.py | 45 ++++++++++ tests/fixtures/security.yaml | 39 ++++++++ tests/fixtures/security_oauth2.yaml | 32 +++++++ tests/integration/test_server.py | 135 ++++++++++++++++++++++++++++ 7 files changed, 271 insertions(+) create mode 100644 asynction/default_handlers.py create mode 100644 tests/fixtures/security.yaml create mode 100644 tests/fixtures/security_oauth2.yaml diff --git a/asynction/default_handlers.py b/asynction/default_handlers.py new file mode 100644 index 0000000..a56e84b --- /dev/null +++ b/asynction/default_handlers.py @@ -0,0 +1,7 @@ +def default_on_connect_handler(*args, **kwargs): + """Injected into api when security is specified by no connect handler is provided""" + + pass + + +DEFAULT_ON_CONNECT_HANDLER = "asynction.default_handlers.default_on_connect_handler" diff --git a/asynction/server.py b/asynction/server.py index c2190f7..27b37d1 100644 --- a/asynction/server.py +++ b/asynction/server.py @@ -14,6 +14,7 @@ from flask import Flask from flask_socketio import SocketIO +from asynction.default_handlers import DEFAULT_ON_CONNECT_HANDLER from asynction.exceptions import ValidationException from asynction.loader import load_handler from asynction.playground_docs import make_docs_blueprint @@ -226,6 +227,14 @@ def _register_handlers( self.on_event(message.name, handler, namespace) + if server_security is not None and ( + channel.x_handlers is None or channel.x_handlers.connect is None + ): + if channel.x_handlers is None: + channel.x_handlers = ChannelHandlers() + if channel.x_handlers.connect is None: + channel.x_handlers.connect = DEFAULT_ON_CONNECT_HANDLER + if channel.x_handlers is not None: self._register_namespace_handlers( namespace, diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py index 6047853..9cfcf49 100644 --- a/tests/fixtures/__init__.py +++ b/tests/fixtures/__init__.py @@ -6,10 +6,14 @@ class FixturePaths(NamedTuple): simple: Path echo: Path simple_with_servers: Path + security: Path + security_oauth2: Path paths = FixturePaths( simple=Path(__file__).parent.joinpath("simple.yml"), echo=Path(__file__).parent.joinpath("echo.yml"), simple_with_servers=Path(__file__).parent.joinpath("simple_with_servers.yml"), + security=Path(__file__).parent.joinpath("security.yaml"), + security_oauth2=Path(__file__).parent.joinpath("security_oauth2.yaml"), ) diff --git a/tests/fixtures/handlers.py b/tests/fixtures/handlers.py index 0d3e664..c5e6718 100644 --- a/tests/fixtures/handlers.py +++ b/tests/fixtures/handlers.py @@ -1,4 +1,8 @@ +import base64 from typing import Any +from typing import Mapping +from typing import Optional +from typing import Sequence from flask import request from flask_socketio import emit @@ -52,3 +56,44 @@ def authenticated_connect() -> None: def echo_failed_validation(e: Exception) -> None: if isinstance(e, ValidationException): emit("echo errors", "Incoming message failed validation") + + +def basic_info( + username: str, password: str, required_scopes: Optional[Sequence[str]] = None +) -> Mapping: + if username != "username" or password != "password": + raise ConnectionRefusedError("Invalid username or password") + + return dict(user=username, scopes=list(required_scopes)) + + +def bearer_info( + token: str, + required_scopes: Optional[Sequence[str]] = None, + bearer_format: Optional[str] = None, +) -> Mapping: + username, password = base64.b64decode(token).decode().split(":") + if username != "username" or password != "password" or bearer_format != "test": + raise ConnectionRefusedError("Invalid username or password") + + return dict(user=username, scopes=list(required_scopes)) + + +def api_key_info( + token: str, + required_scopes: Optional[Sequence[str]] = None, + bearer_format: Optional[str] = None, +) -> Mapping: + username, password = base64.b64decode(token).decode().split(":") + if username != "username" or password != "password": + raise ConnectionRefusedError("Invalid username or password") + + return dict(user=username, scopes=list(required_scopes)) + + +def token_info(token: str) -> Mapping: + username, password = base64.b64decode(token).decode().split(":") + if username != "username" or password != "password": + raise ConnectionRefusedError("Invalid username or password") + + return dict(user=username, scopes=["a", "b"]) diff --git a/tests/fixtures/security.yaml b/tests/fixtures/security.yaml new file mode 100644 index 0000000..4d6dd36 --- /dev/null +++ b/tests/fixtures/security.yaml @@ -0,0 +1,39 @@ +asyncapi: 2.0.0 +info: + title: Test + version: 1.0.0 +servers: + test: + protocol: wss + url: 127.0.0.1/socket.io + security: + - basic: [] + - bearer: ["a"] + - apiKey: ["a"] +channels: + /: + subscribe: + message: + $ref: "#/components/messages/Test" +components: + messages: + Test: + name: test + payload: + type: string + + securitySchemes: + basic: + type: http + scheme: basic + x-basicInfoFunc: tests.fixtures.handlers.basic_info + bearer: + type: http + scheme: bearer + bearerFormat: test + x-apiKeyInfoFunc: tests.fixtures.handlers.bearer_info + apiKey: + type: httpApiKey + in: query + name: api_key + x-apiKeyInfoFunc: tests.fixtures.handlers.api_key_info diff --git a/tests/fixtures/security_oauth2.yaml b/tests/fixtures/security_oauth2.yaml new file mode 100644 index 0000000..2d75cd8 --- /dev/null +++ b/tests/fixtures/security_oauth2.yaml @@ -0,0 +1,32 @@ +asyncapi: 2.0.0 +info: + title: Test + version: 1.0.0 +servers: + test: + protocol: wss + url: 127.0.0.1/socket.io + security: + - oauth2: ["a"] +channels: + /: + subscribe: + message: + $ref: "#/components/messages/Test" +components: + messages: + Test: + name: test + payload: + type: string + + securitySchemes: + oauth2: + type: oauth2 + flows: + implicit: + authorizationUrl: test + scopes: + a: "Test A" + b: "Test B" + x-tokenInfoFunc: tests.fixtures.handlers.token_info diff --git a/tests/integration/test_server.py b/tests/integration/test_server.py index 98d2b06..29d90da 100644 --- a/tests/integration/test_server.py +++ b/tests/integration/test_server.py @@ -1,3 +1,4 @@ +import base64 from enum import Enum from typing import Callable @@ -313,3 +314,137 @@ def test_docs_raw_specification_endpoint( with fixture_paths.simple.open() as f: assert resolve_references(yaml.safe_load(f.read())) == resp.json + + +@pytest.mark.parametrize( + argnames="factory_fixture", + argvalues=[FactoryFixture.ASYNCTION_SOCKET_IO], + ids=["server"], +) +def test_client_fails_to_connect_with_no_auth( + factory_fixture: FactoryFixture, + flask_app: Flask, + fixture_paths: FixturePaths, + request: pytest.FixtureRequest, +): + server_factory: AsynctionFactory = request.getfixturevalue(factory_fixture.value) + + socketio_server = server_factory( + spec_path=fixture_paths.security, server_name="test" + ) + flask_test_client = flask_app.test_client() + + with pytest.raises(ConnectionRefusedError): + socketio_test_client = socketio_server.test_client( + flask_app, flask_test_client=flask_test_client + ) + + assert socketio_test_client.is_connected() is False + + +@pytest.mark.parametrize( + argnames="factory_fixture", + argvalues=[FactoryFixture.ASYNCTION_SOCKET_IO], + ids=["server"], +) +def test_client_connects_with_http_basic_auth( + factory_fixture: FactoryFixture, + flask_app: Flask, + fixture_paths: FixturePaths, + request: pytest.FixtureRequest, +): + server_factory: AsynctionFactory = request.getfixturevalue(factory_fixture.value) + + socketio_server = server_factory( + spec_path=fixture_paths.security, server_name="test" + ) + flask_test_client = flask_app.test_client() + + basic_auth = base64.b64encode("username:password".encode()).decode() + headers = {"Authorization": f"basic {basic_auth}"} + socketio_test_client = socketio_server.test_client( + flask_app, flask_test_client=flask_test_client, headers=headers + ) + + assert socketio_test_client.is_connected() is True + + +@pytest.mark.parametrize( + argnames="factory_fixture", + argvalues=[FactoryFixture.ASYNCTION_SOCKET_IO], + ids=["server"], +) +def test_client_connects_with_http_bearer_auth( + factory_fixture: FactoryFixture, + flask_app: Flask, + fixture_paths: FixturePaths, + request: pytest.FixtureRequest, +): + server_factory: AsynctionFactory = request.getfixturevalue(factory_fixture.value) + + socketio_server = server_factory( + spec_path=fixture_paths.security, server_name="test" + ) + flask_test_client = flask_app.test_client() + + basic_auth = base64.b64encode("username:password".encode()).decode() + headers = {"Authorization": f"bearer {basic_auth}"} + socketio_test_client = socketio_server.test_client( + flask_app, flask_test_client=flask_test_client, headers=headers + ) + + assert socketio_test_client.is_connected() is True + + +@pytest.mark.parametrize( + argnames="factory_fixture", + argvalues=[FactoryFixture.ASYNCTION_SOCKET_IO], + ids=["server"], +) +def test_client_connects_with_http_api_key_auth( + factory_fixture: FactoryFixture, + flask_app: Flask, + fixture_paths: FixturePaths, + request: pytest.FixtureRequest, +): + server_factory: AsynctionFactory = request.getfixturevalue(factory_fixture.value) + + socketio_server = server_factory( + spec_path=fixture_paths.security, server_name="test" + ) + flask_test_client = flask_app.test_client() + + basic_auth = base64.b64encode("username:password".encode()).decode() + query = f"api_key={basic_auth}" + socketio_test_client = socketio_server.test_client( + flask_app, flask_test_client=flask_test_client, query_string=query + ) + + assert socketio_test_client.is_connected() is True + + +@pytest.mark.parametrize( + argnames="factory_fixture", + argvalues=[FactoryFixture.ASYNCTION_SOCKET_IO], + ids=["server"], +) +def test_client_connects_with_oauth2( + factory_fixture: FactoryFixture, + flask_app: Flask, + fixture_paths: FixturePaths, + request: pytest.FixtureRequest, +): + server_factory: AsynctionFactory = request.getfixturevalue(factory_fixture.value) + + socketio_server = server_factory( + spec_path=fixture_paths.security_oauth2, server_name="test" + ) + flask_test_client = flask_app.test_client() + + basic_auth = base64.b64encode("username:password".encode()).decode() + headers = {"Authorization": f"bearer {basic_auth}"} + socketio_test_client = socketio_server.test_client( + flask_app, flask_test_client=flask_test_client, headers=headers + ) + + assert socketio_test_client.is_connected() is True