diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cdf254a..ecbea51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,9 +22,10 @@ jobs: type-check: runs-on: ubuntu-latest - strategy: &strategy + strategy: + fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: &python-versions ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 @@ -51,7 +52,11 @@ jobs: test: runs-on: ubuntu-latest needs: [lint-and-format] - strategy: *strategy + strategy: + fail-fast: false + max-parallel: 1 + matrix: + python-version: *python-versions steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v6 @@ -67,20 +72,19 @@ jobs: uv sync --locked --all-extras --all-packages fi - - name: Initialize Localtunnel + - name: Initialize InstaTunnel id: tunnel run: | - npm install -g localtunnel - npm exec localtunnel -- --port 5000 > tunnel.log 2>&1 & + npm install -g instatunnel + nohup instatunnel 5000 > tunnel.log 2>&1 & - # Poll for the URL - TIMEOUT=15 + TIMEOUT=60 ELAPSED=0 - echo "Waiting for localtunnel to generate URL..." + echo "Waiting for InstaTunnel to generate URL..." - while ! grep -q "https://" tunnel.log; do + while ! grep -qE 'https://[^ ]+\.instatunnel\.my' tunnel.log; do if [ $ELAPSED -ge $TIMEOUT ]; then - echo "Error: Localtunnel timed out after ${TIMEOUT}s" + echo "Error: InstaTunnel timed out after ${TIMEOUT}s" cat tunnel.log exit 1 fi @@ -88,15 +92,15 @@ jobs: ELAPSED=$((ELAPSED + 1)) done - TUNNEL_URL=$(grep -o 'https://[^ ]*' tunnel.log | head -n 1) + TUNNEL_URL=$(grep -oE 'https://[^ ]+\.instatunnel\.my' tunnel.log | head -n 1) echo "url=$TUNNEL_URL" >> $GITHUB_OUTPUT - echo "Localtunnel is live at: $TUNNEL_URL" + echo "InstaTunnel is live at: $TUNNEL_URL" - - name: Upload localtunnel log + - name: Upload InstaTunnel log if: always() uses: actions/upload-artifact@v4 with: - name: localtunnel-log-py${{ matrix.python-version }} + name: instatunnel-log-py${{ matrix.python-version }} path: tunnel.log - name: Run tests diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..396f62c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,33 @@ +# pylint: disable=missing-class-docstring, missing-function-docstring, missing-module-docstring, redefined-outer-name + +import pytest + +from fishjam import FishjamClient, Room, RoomOptions +from fishjam.errors import HTTPError +from tests.support.env import FISHJAM_ID, FISHJAM_MANAGEMENT_TOKEN + + +class _TrackingFishjamClient(FishjamClient): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._tracked_room_ids: list[str] = [] + + def create_room(self, options: RoomOptions | None = None) -> Room: + room = super().create_room(options) + self._tracked_room_ids.append(room.id) + return room + + def cleanup_tracked_rooms(self) -> None: + for room_id in self._tracked_room_ids: + try: + self.delete_room(room_id) + except HTTPError: + pass + self._tracked_room_ids.clear() + + +@pytest.fixture +def room_api(): + client = _TrackingFishjamClient(FISHJAM_ID, FISHJAM_MANAGEMENT_TOKEN) + yield client + client.cleanup_tracked_rooms() diff --git a/tests/support/asyncio_utils.py b/tests/support/asyncio_utils.py index 36707f9..f399720 100644 --- a/tests/support/asyncio_utils.py +++ b/tests/support/asyncio_utils.py @@ -7,25 +7,54 @@ ASSERTION_TIMEOUT = 15.0 -async def assert_events(notifier: FishjamNotifier, event_checks: list): - await _assert_messages(notifier.on_server_notification, event_checks) - - -async def _assert_messages(notifier_callback, message_checks): +async def assert_events( + notifier: FishjamNotifier, + event_checks: list, + *, + room_id_future: asyncio.Future | None = None, +): + await _assert_messages( + notifier.on_server_notification, event_checks, room_id_future + ) + + +async def _assert_messages(notifier_callback, message_checks, room_id_future): success_event = asyncio.Event() + pending: list = [] + room_id_holder: dict = {"value": None, "set": False} - @notifier_callback - def handle_message(message): + def _consume(message): if len(message_checks) > 0: expected_msg = message_checks[0] if message == expected_msg or isinstance(message, expected_msg): message_checks.pop(0) - if message_checks == []: success_event.set() + @notifier_callback + def handle_message(message): + if not room_id_holder["set"]: + pending.append(message) + return + + expected_room_id = room_id_holder["value"] + if expected_room_id is not None: + if getattr(message, "room_id", None) != expected_room_id: + return + + _consume(message) + + async def _wait_for_success(): + if room_id_future is not None: + room_id_holder["value"] = await room_id_future + room_id_holder["set"] = True + for msg in pending: + handle_message(msg) + pending.clear() + await success_event.wait() + try: - await asyncio.wait_for(success_event.wait(), ASSERTION_TIMEOUT) + await asyncio.wait_for(_wait_for_success(), ASSERTION_TIMEOUT) except asyncio.exceptions.TimeoutError as exc: raise asyncio.exceptions.TimeoutError( f"{message_checks[0]} hasn't been received within timeout" diff --git a/tests/test_notifier.py b/tests/test_notifier.py index 9dd93aa..7ff7727 100644 --- a/tests/test_notifier.py +++ b/tests/test_notifier.py @@ -93,11 +93,6 @@ def handle_notitifcation(_notification): await asyncio.gather(notifier_task, return_exceptions=True) -@pytest.fixture -def room_api(): - return FishjamClient(FISHJAM_ID, FISHJAM_MANAGEMENT_TOKEN) - - @pytest.fixture def notifier(): notifier = FishjamNotifier( @@ -115,8 +110,9 @@ async def test_room_created_deleted( ): event_checks = [ServerMessageRoomCreated, ServerMessageRoomDeleted] + room_id_future: asyncio.Future = asyncio.get_running_loop().create_future() assert_task = asyncio.ensure_future( - assert_events(notifier, event_checks.copy()) + assert_events(notifier, event_checks.copy(), room_id_future=room_id_future) ) notifier_task = asyncio.ensure_future(notifier.connect()) try: @@ -124,6 +120,7 @@ async def test_room_created_deleted( options = RoomOptions(webhook_url=WEBHOOK_URL) room = room_api.create_room(options=options) + room_id_future.set_result(room.id) room_api.delete_room(room.id) @@ -148,8 +145,9 @@ async def test_peer_connected_disconnected( ServerMessageRoomDeleted, ] + room_id_future: asyncio.Future = asyncio.get_running_loop().create_future() assert_task = asyncio.ensure_future( - assert_events(notifier, event_checks.copy()) + assert_events(notifier, event_checks.copy(), room_id_future=room_id_future) ) notifier_task = asyncio.ensure_future(notifier.connect()) tasks = [assert_task, notifier_task] @@ -158,6 +156,7 @@ async def test_peer_connected_disconnected( options = RoomOptions(webhook_url=WEBHOOK_URL) room = room_api.create_room(options=options) + room_id_future.set_result(room.id) peer, token = room_api.create_peer(room.id) peer_socket = PeerSocket(fishjam_url=FISHJAM_ID) @@ -185,12 +184,14 @@ async def test_peer_connected_room_deleted( ServerMessageRoomCreated, ServerMessagePeerAdded, ServerMessagePeerConnected, + ServerMessagePeerDisconnected, ServerMessagePeerDeleted, ServerMessageRoomDeleted, ] + room_id_future: asyncio.Future = asyncio.get_running_loop().create_future() assert_task = asyncio.ensure_future( - assert_events(notifier, event_checks.copy()) + assert_events(notifier, event_checks.copy(), room_id_future=room_id_future) ) notifier_task = asyncio.ensure_future(notifier.connect()) tasks = [assert_task, notifier_task] @@ -199,6 +200,7 @@ async def test_peer_connected_room_deleted( options = RoomOptions(webhook_url=WEBHOOK_URL) room = room_api.create_room(options=options) + room_id_future.set_result(room.id) _peer, token = room_api.create_peer(room.id) peer_socket = PeerSocket(fishjam_url=FISHJAM_ID) diff --git a/tests/test_room_api.py b/tests/test_room_api.py index 2416e34..d010e2f 100644 --- a/tests/test_room_api.py +++ b/tests/test_room_api.py @@ -45,9 +45,7 @@ def test_invalid_token(self): with pytest.raises(UnauthorizedError): room_api.create_room() - def test_valid_token(self): - room_api = FishjamClient(FISHJAM_ID, FISHJAM_MANAGEMENT_TOKEN) - + def test_valid_token(self, room_api: FishjamClient): room = room_api.create_room() all_rooms = room_api.get_all_rooms() @@ -84,11 +82,6 @@ def mock_send(request, **kwargs): assert captured_headers["x-fishjam-api-client"] == expected_header_value -@pytest.fixture -def room_api(): - return FishjamClient(FISHJAM_ID, FISHJAM_MANAGEMENT_TOKEN) - - class TestCreateRoom: def test_no_params(self, room_api: FishjamClient): room = room_api.create_room()