|
| 1 | +# Copyright 2026 LiveKit, Inc. |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +import asyncio |
| 16 | +from unittest.mock import AsyncMock |
| 17 | + |
| 18 | +import aiohttp |
| 19 | +import pytest |
| 20 | +from aiohttp import web |
| 21 | +from aiohttp.test_utils import TestServer |
| 22 | + |
| 23 | +from livekit.api import CreateRoomRequest, LiveKitAPI, Room |
| 24 | +from livekit.api import twirp_client |
| 25 | +from livekit.api.twirp_client import REQUEST_ID_HEADER, TwirpClient |
| 26 | + |
| 27 | + |
| 28 | +@pytest.mark.parametrize("base_path", ["", "/", "/livekit", "/proxy/livekit/"]) |
| 29 | +@pytest.mark.parametrize("scheme", ["http", "ws"]) |
| 30 | +def test_api_request_preserves_base_path(base_path: str, scheme: str) -> None: |
| 31 | + async def run() -> None: |
| 32 | + async def create_room(request: web.Request) -> web.Response: |
| 33 | + message = CreateRoomRequest.FromString(await request.read()) |
| 34 | + assert message.name == "test-room" |
| 35 | + assert request.headers["Authorization"] == "Bearer test-token" |
| 36 | + return web.Response(body=Room(name=message.name).SerializeToString()) |
| 37 | + |
| 38 | + app = web.Application() |
| 39 | + app.router.add_post( |
| 40 | + f"{base_path.rstrip('/')}/twirp/livekit.RoomService/CreateRoom", create_room |
| 41 | + ) |
| 42 | + async with TestServer(app) as server: |
| 43 | + url = str(server.make_url(base_path)).replace("http://", f"{scheme}://", 1) |
| 44 | + async with LiveKitAPI.with_token("test-token", url, failover=False) as api: |
| 45 | + room = await api.room.create_room(CreateRoomRequest(name="test-room")) |
| 46 | + assert room.name == "test-room" |
| 47 | + |
| 48 | + asyncio.run(run()) |
| 49 | + |
| 50 | + |
| 51 | +@pytest.mark.parametrize("prefix", ["twirp", "custom/twirp"]) |
| 52 | +def test_failover_preserves_base_path(monkeypatch: pytest.MonkeyPatch, prefix: str) -> None: |
| 53 | + async def run() -> None: |
| 54 | + requests = [] |
| 55 | + |
| 56 | + async def record_request(request: web.Request) -> None: |
| 57 | + requests.append( |
| 58 | + (request.path, request.headers[REQUEST_ID_HEADER], await request.read()) |
| 59 | + ) |
| 60 | + |
| 61 | + async def primary(request: web.Request) -> web.Response: |
| 62 | + await record_request(request) |
| 63 | + return web.json_response({"code": "unavailable", "msg": "retry"}, status=503) |
| 64 | + |
| 65 | + async def fallback(request: web.Request) -> web.Response: |
| 66 | + await record_request(request) |
| 67 | + return web.Response(body=Room(name="test-room").SerializeToString()) |
| 68 | + |
| 69 | + route = f"/proxy/livekit/{prefix}/livekit.RoomService/CreateRoom" |
| 70 | + primary_app = web.Application() |
| 71 | + primary_app.router.add_post(route, primary) |
| 72 | + fallback_app = web.Application() |
| 73 | + fallback_app.router.add_post(route, fallback) |
| 74 | + async with TestServer(primary_app) as first, TestServer(fallback_app) as second: |
| 75 | + discover = AsyncMock(return_value=[str(second.make_url("")).rstrip("/")]) |
| 76 | + monkeypatch.setattr(twirp_client._REGION_CACHE, "region_origins", discover) |
| 77 | + async with aiohttp.ClientSession() as session: |
| 78 | + client = TwirpClient( |
| 79 | + session, |
| 80 | + str(first.make_url("/proxy/livekit/")), |
| 81 | + "livekit", |
| 82 | + prefix=prefix, |
| 83 | + _failover_force=True, |
| 84 | + _failover_backoff=0, |
| 85 | + ) |
| 86 | + room = await client.request( |
| 87 | + "RoomService", "CreateRoom", CreateRoomRequest(name="test-room"), {}, Room |
| 88 | + ) |
| 89 | + assert room.name == "test-room" |
| 90 | + assert len(requests) == 2 |
| 91 | + assert requests[0] == requests[1] |
| 92 | + assert requests[0][0] == route |
| 93 | + assert requests[0][1] |
| 94 | + discover.assert_awaited_once() |
| 95 | + |
| 96 | + asyncio.run(run()) |
0 commit comments