diff --git a/.gitignore b/.gitignore index 6f19aa6..67b9b90 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,6 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# Temporary files +/tmp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b2e4ed5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "protos"] + path = protos + url = https://github.com/jellyfish-dev/protos.git diff --git a/README.md b/README.md index b904a51..e62dcb7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![CircleCI](https://dl.circleci.com/status-badge/img/gh/jellyfish-dev/python-server-sdk/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/jellyfish-dev/python-server-sdk/tree/main) -Python server SDK for the [Jellyfish](https://github.com/jellyfish-dev/jellyfish) media server. +Python server SDK for the [Jellyfish Media Server](https://github.com/jellyfish-dev/jellyfish). Read the docs [here](https://jellyfish-dev.github.io/python-server-sdk/jellyfish.html) @@ -15,15 +15,22 @@ pip install git+https://github.com/jellyfish-dev/python-server-sdk ## Usage -First create a `RoomApi` instance, providing the jellyfish server address and api token +The SDK exports two main classes for interacting with Jellyfish server: +`RoomApi` and `Notifier`. + +`RoomApi` wraps http REST api calls, while `Notifier` is responsible for receiving real-time updates from the server. + +#### RoomApi + +Create a `RoomApi` instance, providing the jellyfish server address and api token ```python from jellyfish import RoomApi -room_api = RoomApi(server_address='http://localhost:5002', server_api_token='development') +room_api = RoomApi(server_address='localhost:5002', server_api_token='development') ``` -You can use it to interact with Jellyfish managing rooms, peers and components +You can use it to interact with Jellyfish, manage rooms, peers and components ```python # Create a room @@ -43,6 +50,60 @@ component_hls = room_api.add_component(room.id, options=ComponentOptionsHLS()) # Component(actual_instance=ComponentHLS(id='c0dfab50-cafd-438d-985e-7b8f97ae55e3', metadata=ComponentMetadataHLS(low_latency=False, playable=False), type='hls')) ``` +#### Notifier + +Create `Notifier` instance +```python +from jellyfish import Notifier + +notifier = Notifier(server_address='localhost:5002', server_api_token='development') +``` + +Then define handlers for incoming messages +```python +@notifier.on_server_notification +def handle_notification(server_notification): + print(f'Received a notification: {notification}') + +@notifier.on_metrics +def handle_metrics(metrics_report): + print(f'Received WebRTC metrics: {metrics_report.metrics}') +``` + +After that you can start the notifier +```python +async def test_notifier(): + notifier_task = asyncio.create_task(notifier.connect()) + + # Wait for notifier to be ready to receive messages + await notifier.wait_ready() + + # Create a room to trigger a server notification + room_api = RoomApi() + room_api.create_room() + + await notifier_task + +asyncio.run(test_notifier()) + +# Received a notification: ServerMessageRoomCreated(room_id='69a3fd1a-6a4d-47bc-ae54-0c72b0d05e29') +# Received WebRTC metrics: ServerMessageMetricsReport(metrics='{}') +``` + +## Testing + +You can test the SDK against a local instance of Jellyfish by running +```console +pytest +``` + +Make sure to use the default configuration for Jellyfish + +Alternatively you can test using Docker +```console +docker-compose -f docker-compose-test.yaml run test +``` + ## Copyright and License Copyright 2023, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=jellyfish) diff --git a/compile_proto.sh b/compile_proto.sh new file mode 100755 index 0000000..6d3719e --- /dev/null +++ b/compile_proto.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Terminate on errors +set -e + + +printf "Synchronising submodules... " +git submodule sync --recursive >> /dev/null +git submodule update --recursive --remote --init >> /dev/null +printf "DONE\n\n" + +server_file="./protos/jellyfish/server_notifications.proto" +printf "Compiling: file $server_file" +protoc -I . --python_betterproto_out=./jellyfish/events/_protos $server_file +printf "\tDONE\n" + +peer_file="./protos/jellyfish/peer_notifications.proto" +printf "Compiling: file $peer_file" +protoc -I . --python_betterproto_out=./tests/support/protos $peer_file +printf "\tDONE\n" diff --git a/dev-requirements.txt b/dev-requirements.txt index 8139065..544782c 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,9 +1,12 @@ aenum==3.1.15 +betterproto==1.2.5 pydantic==1.10.12 pytest==7.1.3 +pytest-asyncio==0.21.1 python_dateutil==2.8.2 setuptools==67.6.1 typing_extensions==4.7.1 urllib3 >= 1.25.3, < 2 pylint==2.17.5 pdoc==14.1.0 +websockets==11.0.3 diff --git a/docker-compose-test.yaml b/docker-compose-test.yaml index a1a69fe..5a49005 100644 --- a/docker-compose-test.yaml +++ b/docker-compose-test.yaml @@ -13,15 +13,14 @@ services: timeout: 2s start_period: 30s environment: - VIRTUAL_HOST: "jellyfish" - USE_INTEGRATED_TURN: "true" - INTEGRATED_TURN_IP: "${INTEGRATED_TURN_IP:-127.0.0.1}" - INTEGRATED_TURN_LISTEN_IP: "0.0.0.0" - INTEGRATED_TURN_PORT_RANGE: "50000-50050" - INTEGRATED_TCP_TURN_PORT: "49999" - SERVER_API_TOKEN: "development" - PORT: 5002 - SECRET_KEY_BASE: "super-secret-key" + JF_HOST: "jellyfish" + JF_INTEGRATED_TURN_IP: "${INTEGRATED_TURN_IP:-127.0.0.1}" + JF_INTEGRATED_TURN_LISTEN_IP: "0.0.0.0" + JF_INTEGRATED_TURN_PORT_RANGE: "50000-50050" + JF_INTEGRATED_TCP_TURN_PORT: "49999" + JF_SERVER_API_TOKEN: "development" + JF_PORT: 5002 + JF_SECRET_KEY_BASE: "super-secret-key" ports: - "5002:5002" - "49999:49999" @@ -31,9 +30,9 @@ services: test: image: python:3.8-alpine3.18 - command: sh -c "cd app/ && pip install -e . && pytest" + command: sh -c "cd app/ && pip install -r dev-requirements.txt && pytest -s" environment: - - DOCKER_TEST=TRUE + DOCKER_TEST: "TRUE" volumes: - .:/app networks: diff --git a/jellyfish/__init__.py b/jellyfish/__init__.py index f472b0a..cf0366f 100644 --- a/jellyfish/__init__.py +++ b/jellyfish/__init__.py @@ -6,17 +6,27 @@ from pydantic.error_wrappers import ValidationError +# API +from jellyfish._room_api import RoomApi +from jellyfish._notifier import Notifier + +# Models from jellyfish._openapi_client import ( Room, RoomConfig, Peer, Component, ComponentHLS, ComponentRTSP, ComponentOptions, ComponentOptionsRTSP, ComponentOptionsHLS, PeerOptionsWebRTC) +# Server Messages +from jellyfish import events + +# Exceptions from jellyfish._openapi_client.exceptions import ( UnauthorizedException, NotFoundException, BadRequestException) -from jellyfish._room_api import RoomApi -__all__ = ['RoomApi', 'Room', 'Peer', 'Component', 'ComponentHLS', 'ComponentRTSP', - 'ComponentOptionsHLS', 'RoomConfig', 'ComponentOptions', 'ComponentOptionsRTSP', - 'PeerOptionsWebRTC', 'UnauthorizedException', 'NotFoundException', 'BadRequestException'] +__all__ = [ + 'RoomApi', 'Notifier', 'Room', 'Peer', 'Component', 'ComponentHLS', 'ComponentRTSP', + 'ComponentOptionsHLS', 'RoomConfig', 'ComponentOptions', 'ComponentOptionsRTSP', + 'PeerOptionsWebRTC', 'events', 'UnauthorizedException', 'NotFoundException', + 'BadRequestException'] __docformat__ = "restructuredtext" diff --git a/jellyfish/_notifier.py b/jellyfish/_notifier.py new file mode 100644 index 0000000..c9e01de --- /dev/null +++ b/jellyfish/_notifier.py @@ -0,0 +1,133 @@ +''' +Notifier listening to WebSocket events +''' + +import asyncio +from typing import Callable, Any + +import betterproto + +from websockets import client +from websockets.exceptions import ConnectionClosed + +from jellyfish.events import ( + ServerMessage, ServerMessageAuthRequest, ServerMessageAuthenticated, + ServerMessageSubscribeRequest, ServerMessageEventType, ServerMessageSubscribeResponse, + ServerMessageMetricsReport) + + +class Notifier: + ''' + Allows for receiving WebSocket messages from Jellyfish. + ''' + + def __init__(self, + server_address: str = 'localhost:5002', server_api_token: str = 'development'): + self._server_address = server_address + self._server_api_token = server_api_token + self._websocket = None + self._ready = False + + self._ready_event: asyncio.Event = None + + self._notification_handler: Callable = None + self._metrics_handler: Callable = None + + def on_server_notification(self, handler: Callable[[Any], None]): + ''' + Decorator used for defining handler for ServerNotifications + i.e. all messages other than `ServerMessageMetricsReport`. + ''' + self._notification_handler = handler + return handler + + def on_metrics(self, handler: Callable[[ServerMessageMetricsReport], None]): + ''' + Decorator used for defining handler for `ServerMessageMetricsReport`. + ''' + self._metrics_handler = handler + return handler + + async def connect(self): + ''' + A coroutine which connects Notifier to Jellyfish and listens for all incoming + messages from the Jellyfish. + + It runs until the connection isn't closed. + + The incoming messages are handled by the functions defined using the + `on_server_notification` and `on_metrics` decorators. + + The handlers have to be defined before calling `connect`, + otherwise the messages won't be received. + ''' + address = f'ws://{self._server_address}/socket/server/websocket' + async with client.connect(address) as websocket: + try: + self._websocket = websocket + await self._authenticate() + + if self._notification_handler: + await self._subscribe_event( + event=ServerMessageEventType.EVENT_TYPE_SERVER_NOTIFICATION) + + if self._metrics_handler: + await self._subscribe_event(event=ServerMessageEventType.EVENT_TYPE_METRICS) + + self._ready = True + if self._ready_event: + self._ready_event.set() + + await self._receive_loop() + finally: + self._websocket = None + + async def wait_ready(self) -> True: + ''' + Waits until the notifier is connected and authenticated to Jellyfish. + + If already connected, returns `True` immediately. + ''' + if self._ready: + return True + + if self._ready_event is None: + self._ready_event = asyncio.Event() + + await self._ready_event.wait() + + async def _authenticate(self): + msg = ServerMessage(auth_request=ServerMessageAuthRequest(token=self._server_api_token)) + await self._websocket.send(bytes(msg)) + + try: + message = await self._websocket.recv() + except ConnectionClosed as exception: + if 'invalid token' in str(exception): + raise RuntimeError('Invalid server_api_token') from exception + raise + + message = ServerMessage().parse(message) + + _type, message = betterproto.which_one_of(message, 'content') + assert isinstance(message, ServerMessageAuthenticated) + + async def _receive_loop(self): + while True: + message = await self._websocket.recv() + message = ServerMessage().parse(message) + _which, message = betterproto.which_one_of(message, 'content') + + if isinstance(message, ServerMessageMetricsReport): + self._metrics_handler(message) + else: + self._notification_handler(message) + + async def _subscribe_event(self, event: ServerMessageEventType): + request = ServerMessage(subscribe_request=ServerMessageSubscribeRequest(event)) + + await self._websocket.send(bytes(request)) + message = await self._websocket.recv() + message = ServerMessage().parse(message) + _which, message = betterproto.which_one_of(message, "content") + assert isinstance(message, ServerMessageSubscribeResponse) diff --git a/jellyfish/_room_api.py b/jellyfish/_room_api.py index f59c909..e501981 100644 --- a/jellyfish/_room_api.py +++ b/jellyfish/_room_api.py @@ -1,6 +1,6 @@ -""" - RoomApi used to manage rooms -""" +''' +RoomApi used to manage rooms +''' from typing import Union, Literal @@ -20,7 +20,7 @@ def __init__(self, Create RoomApi instance, providing the jellyfish address and api token. ''' self._configuration = jellyfish_api.Configuration( - host=server_address, + host=f'http://{server_address}', access_token=server_api_token ) diff --git a/jellyfish/events/__init__.py b/jellyfish/events/__init__.py new file mode 100644 index 0000000..39b8239 --- /dev/null +++ b/jellyfish/events/__init__.py @@ -0,0 +1,21 @@ +''' +Server events being sent Jellyfish +''' + +# Private messages +from jellyfish.events._protos.jellyfish import ( + ServerMessage, ServerMessageAuthenticated, ServerMessageAuthRequest, ServerMessageEventType, + ServerMessageSubscribeResponse, ServerMessageSubscribeRequest) + +# Exported messages +from jellyfish.events._protos.jellyfish import ( + ServerMessageComponentCrashed, ServerMessageHlsPlayable, + ServerMessageMetricsReport, ServerMessagePeerCrashed, ServerMessagePeerConnected, + ServerMessagePeerDisconnected, ServerMessageRoomCrashed, ServerMessageRoomDeleted, + ServerMessageRoomCreated) + +__all__ = [ + 'ServerMessageComponentCrashed', 'ServerMessageHlsPlayable', + 'ServerMessageMetricsReport', 'ServerMessagePeerCrashed', 'ServerMessagePeerConnected', + 'ServerMessagePeerDisconnected', 'ServerMessageRoomCrashed', 'ServerMessageRoomDeleted', + 'ServerMessageRoomCreated'] diff --git a/jellyfish/events/_protos/__init__.py b/jellyfish/events/_protos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/jellyfish/events/_protos/jellyfish.py b/jellyfish/events/_protos/jellyfish.py new file mode 100644 index 0000000..63307e2 --- /dev/null +++ b/jellyfish/events/_protos/jellyfish.py @@ -0,0 +1,125 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# sources: protos/jellyfish/server_notifications.proto +# plugin: python-betterproto +from dataclasses import dataclass + +import betterproto + + +class ServerMessageEventType(betterproto.Enum): + EVENT_TYPE_UNSPECIFIED = 0 + EVENT_TYPE_SERVER_NOTIFICATION = 1 + EVENT_TYPE_METRICS = 2 + + +@dataclass +class ServerMessage(betterproto.Message): + room_crashed: "ServerMessageRoomCrashed" = betterproto.message_field( + 1, group="content" + ) + peer_connected: "ServerMessagePeerConnected" = betterproto.message_field( + 2, group="content" + ) + peer_disconnected: "ServerMessagePeerDisconnected" = betterproto.message_field( + 3, group="content" + ) + peer_crashed: "ServerMessagePeerCrashed" = betterproto.message_field( + 4, group="content" + ) + component_crashed: "ServerMessageComponentCrashed" = betterproto.message_field( + 5, group="content" + ) + authenticated: "ServerMessageAuthenticated" = betterproto.message_field( + 6, group="content" + ) + auth_request: "ServerMessageAuthRequest" = betterproto.message_field( + 7, group="content" + ) + subscribe_request: "ServerMessageSubscribeRequest" = betterproto.message_field( + 8, group="content" + ) + subscribe_response: "ServerMessageSubscribeResponse" = betterproto.message_field( + 9, group="content" + ) + room_created: "ServerMessageRoomCreated" = betterproto.message_field( + 10, group="content" + ) + room_deleted: "ServerMessageRoomDeleted" = betterproto.message_field( + 11, group="content" + ) + metrics_report: "ServerMessageMetricsReport" = betterproto.message_field( + 12, group="content" + ) + hls_playable: "ServerMessageHlsPlayable" = betterproto.message_field( + 13, group="content" + ) + + +@dataclass +class ServerMessageRoomCrashed(betterproto.Message): + room_id: str = betterproto.string_field(1) + + +@dataclass +class ServerMessagePeerConnected(betterproto.Message): + room_id: str = betterproto.string_field(1) + peer_id: str = betterproto.string_field(2) + + +@dataclass +class ServerMessagePeerDisconnected(betterproto.Message): + room_id: str = betterproto.string_field(1) + peer_id: str = betterproto.string_field(2) + + +@dataclass +class ServerMessagePeerCrashed(betterproto.Message): + room_id: str = betterproto.string_field(1) + peer_id: str = betterproto.string_field(2) + + +@dataclass +class ServerMessageComponentCrashed(betterproto.Message): + room_id: str = betterproto.string_field(1) + component_id: str = betterproto.string_field(2) + + +@dataclass +class ServerMessageAuthenticated(betterproto.Message): + pass + + +@dataclass +class ServerMessageAuthRequest(betterproto.Message): + token: str = betterproto.string_field(1) + + +@dataclass +class ServerMessageSubscribeRequest(betterproto.Message): + event_type: "ServerMessageEventType" = betterproto.enum_field(1) + + +@dataclass +class ServerMessageSubscribeResponse(betterproto.Message): + event_type: "ServerMessageEventType" = betterproto.enum_field(1) + + +@dataclass +class ServerMessageRoomCreated(betterproto.Message): + room_id: str = betterproto.string_field(1) + + +@dataclass +class ServerMessageRoomDeleted(betterproto.Message): + room_id: str = betterproto.string_field(1) + + +@dataclass +class ServerMessageMetricsReport(betterproto.Message): + metrics: str = betterproto.string_field(1) + + +@dataclass +class ServerMessageHlsPlayable(betterproto.Message): + room_id: str = betterproto.string_field(1) + component_id: str = betterproto.string_field(2) diff --git a/protos b/protos new file mode 160000 index 0000000..a6f6971 --- /dev/null +++ b/protos @@ -0,0 +1 @@ +Subproject commit a6f69712da283d13ae8b6e828694b48fce92a9e5 diff --git a/pylintrc b/pylintrc index 594df76..33374d2 100644 --- a/pylintrc +++ b/pylintrc @@ -52,7 +52,7 @@ ignore=CVS # ignore-list. The regex matches against paths and can be in Posix or Windows # format. Because '\\' represents the directory delimiter on Windows systems, # it can't be used as an escape character. -ignore-paths=jellyfish/_openapi_client/ +ignore-paths=jellyfish/_openapi_client/,jellyfish/events/_protos/,tests/support/protos # Files or directories matching the regular expression patterns are skipped. # The regex matches against base names, not paths. The default value ignores diff --git a/pyproject.toml b/pyproject.toml index 5f049d4..2d69851 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,13 +15,14 @@ classifiers = [ ] dependencies = [ "aenum==3.1.15", + "betterproto==1.2.5", "pydantic==1.10.12", "pytest==7.1.3", "python_dateutil==2.8.2", "setuptools==67.6.1", "typing_extensions==4.7.1", "urllib3 >= 1.25.3, < 2", - "pylint==2.17.5" + "websockets==11.0.3", ] [project.urls] diff --git a/tests/support/__init__.py b/tests/support/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/support/asyncio_utils.py b/tests/support/asyncio_utils.py new file mode 100644 index 0000000..acf10d4 --- /dev/null +++ b/tests/support/asyncio_utils.py @@ -0,0 +1,42 @@ +# pylint: disable=locally-disabled, missing-class-docstring, missing-function-docstring, redefined-outer-name, too-few-public-methods, missing-module-docstring + +import asyncio + +from jellyfish import Notifier + +ASSERTION_TIMEOUT = 2. + + +async def assert_events(notifier: Notifier, event_checks: list): + await _assert_messages(notifier.on_server_notification, event_checks) + + +async def assert_metrics(notifier: Notifier, metrics_checks: list): + await _assert_messages(notifier.on_metrics, metrics_checks) + + +async def _assert_messages(notifier_callback, message_checks): + success_event = asyncio.Event() + + @notifier_callback + def handle_message(message): + expected_msg = message_checks[0] + if message == expected_msg or isinstance(message, expected_msg): + message_checks.pop(0) + + if message_checks == []: + success_event.set() + + try: + await asyncio.wait_for(success_event.wait(), ASSERTION_TIMEOUT) + except asyncio.exceptions.TimeoutError as exc: + raise asyncio.exceptions.TimeoutError( + f"{message_checks[0]} hasn't been received within timeout") from exc + + +async def cancel(task): + task.cancel() + try: + await task + except asyncio.exceptions.CancelledError: + pass diff --git a/tests/support/peer_socket.py b/tests/support/peer_socket.py new file mode 100644 index 0000000..b7abf5c --- /dev/null +++ b/tests/support/peer_socket.py @@ -0,0 +1,50 @@ +# pylint: disable=locally-disabled, missing-class-docstring, missing-function-docstring, redefined-outer-name, too-few-public-methods, missing-module-docstring + +import asyncio +import betterproto + +from websockets import client +from websockets.exceptions import ConnectionClosedOK + +from tests.support.protos.jellyfish import ( + PeerMessage, PeerMessageAuthRequest, PeerMessageAuthenticated) + + +class PeerSocket: + def __init__(self, server_address): + self._server_address = server_address + + self._ready = False + self._ready_event = None + + async def connect(self, token): + async with client.connect(f'ws://{self._server_address}/socket/peer/websocket') \ + as websocket: + msg = PeerMessage(auth_request=PeerMessageAuthRequest(token=token)) + await websocket.send(bytes(msg)) + + try: + message = await websocket.recv() + except ConnectionClosedOK as exception: + raise RuntimeError from exception + + message = PeerMessage().parse(message) + + _type, message = betterproto.which_one_of(message, 'content') + assert isinstance(message, PeerMessageAuthenticated) + + self._ready = True + if self._ready_event: + self._ready_event.set() + + await websocket.wait_closed() + + async def wait_ready(self): + # pylint: disable=duplicate-code + if self._ready: + return + + if self._ready_event is None: + self._ready_event = asyncio.Event() + + await self._ready_event.wait() diff --git a/tests/support/protos/__init__.py b/tests/support/protos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/support/protos/jellyfish.py b/tests/support/protos/jellyfish.py new file mode 100644 index 0000000..67176f5 --- /dev/null +++ b/tests/support/protos/jellyfish.py @@ -0,0 +1,32 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# sources: protos/jellyfish/peer_notifications.proto +# plugin: python-betterproto +from dataclasses import dataclass + +import betterproto + + +@dataclass +class PeerMessage(betterproto.Message): + authenticated: "PeerMessageAuthenticated" = betterproto.message_field( + 1, group="content" + ) + auth_request: "PeerMessageAuthRequest" = betterproto.message_field( + 2, group="content" + ) + media_event: "PeerMessageMediaEvent" = betterproto.message_field(3, group="content") + + +@dataclass +class PeerMessageAuthenticated(betterproto.Message): + pass + + +@dataclass +class PeerMessageAuthRequest(betterproto.Message): + token: str = betterproto.string_field(1) + + +@dataclass +class PeerMessageMediaEvent(betterproto.Message): + data: str = betterproto.string_field(1) diff --git a/tests/test_notifier.py b/tests/test_notifier.py new file mode 100644 index 0000000..ebe9170 --- /dev/null +++ b/tests/test_notifier.py @@ -0,0 +1,116 @@ +# pylint: disable=locally-disabled, missing-class-docstring, missing-function-docstring, redefined-outer-name, too-few-public-methods, missing-module-docstring + +import os +import asyncio + +import pytest + +from jellyfish import Notifier, RoomApi, PeerOptionsWebRTC +from jellyfish.events import (ServerMessageRoomCreated, ServerMessageRoomDeleted, + ServerMessagePeerConnected, ServerMessagePeerDisconnected, + ServerMessageMetricsReport) + +from tests.support.peer_socket import PeerSocket +from tests.support.asyncio_utils import assert_events, assert_metrics, cancel + + +HOST = 'jellyfish' if os.getenv('DOCKER_TEST') == 'TRUE' else 'localhost' +SERVER_ADDRESS = f'{HOST}:5002' +SERVER_API_TOKEN = 'development' + + +class TestConnectingToServer: + @pytest.mark.asyncio + async def test_valid_credentials(self): + notifier = Notifier(server_address=SERVER_ADDRESS, + server_api_token=SERVER_API_TOKEN) + + notifier_task = asyncio.create_task(notifier.connect()) + await notifier.wait_ready() + # pylint: disable=protected-access + assert notifier._websocket.open + + await cancel(notifier_task) + + @pytest.mark.asyncio + async def test_invalid_credentials(self): + notifier = Notifier(server_address=SERVER_ADDRESS, + server_api_token='wrong_token') + + task = asyncio.create_task(notifier.connect()) + + with pytest.raises(RuntimeError): + await task + + +@pytest.fixture +def room_api(): + return RoomApi(server_address=SERVER_ADDRESS, server_api_token=SERVER_API_TOKEN) + + +@pytest.fixture +def notifier(): + return Notifier(server_address=SERVER_ADDRESS, server_api_token=SERVER_API_TOKEN) + + +class TestReceivingNotifications: + @pytest.mark.asyncio + async def test_room_created_deleted(self, room_api: RoomApi, notifier: Notifier): + event_checks = [ + ServerMessageRoomCreated, + ServerMessageRoomDeleted + ] + assert_task = asyncio.create_task(assert_events(notifier, event_checks)) + + notifier_task = asyncio.create_task(notifier.connect()) + await notifier.wait_ready() + + _, room = room_api.create_room() + room_api.delete_room(room.id) + + await assert_task + await cancel(notifier_task) + + @pytest.mark.asyncio + async def test_peer_connected_disconnected(self, room_api: RoomApi, notifier: Notifier): + event_checks = [ + ServerMessagePeerConnected, + ServerMessagePeerDisconnected + ] + assert_task = asyncio.create_task(assert_events(notifier, event_checks)) + + notifier_task = asyncio.create_task(notifier.connect()) + await notifier.wait_ready() + + _, room = room_api.create_room() + peer_token, _peer = room_api.add_peer(room.id, options=PeerOptionsWebRTC()) + + peer_socket = PeerSocket(server_address=SERVER_ADDRESS) + peer_task = asyncio.create_task(peer_socket.connect(peer_token)) + + await peer_socket.wait_ready() + + room_api.delete_room(room.id) + + await assert_task + await cancel(peer_task) + await cancel(notifier_task) + + +class TestReceivingMetrics: + @pytest.mark.asyncio + async def test_metrics_with_one_peer(self, room_api: RoomApi, notifier: Notifier): + _, room = room_api.create_room() + peer_token, _peer = room_api.add_peer(room.id, PeerOptionsWebRTC()) + + peer_socket = PeerSocket(server_address=SERVER_ADDRESS) + peer_task = asyncio.create_task(peer_socket.connect(peer_token)) + + await peer_socket.wait_ready() + + assert_task = asyncio.create_task(assert_metrics(notifier, [ServerMessageMetricsReport])) + notifier_task = asyncio.create_task(notifier.connect()) + + await assert_task + await cancel(peer_task) + await cancel(notifier_task) diff --git a/tests/test_room_api.py b/tests/test_room_api.py index f6c65d4..d230c6d 100644 --- a/tests/test_room_api.py +++ b/tests/test_room_api.py @@ -1,8 +1,5 @@ -# pylint: disable=locally-disabled, missing-class-docstring, missing-function-docstring, redefined-outer-name, too-few-public-methods +# pylint: disable=locally-disabled, missing-class-docstring, missing-function-docstring, redefined-outer-name, too-few-public-methods, missing-module-docstring -""" - Tests room api -""" import os @@ -18,7 +15,7 @@ HOST = 'jellyfish' if os.getenv('DOCKER_TEST') == 'TRUE' else 'localhost' -SERVER_ADDRESS = f'http://{HOST}:5002' +SERVER_ADDRESS = f'{HOST}:5002' SERVER_API_TOKEN = 'development' MAX_PEERS = 10