7.8 KiB
LiveKit SDK test server
A stateless, per-request programmable mock of the LiveKit server HTTP API. It exists so the server SDKs (Go, Rust, Python, Node, Kotlin, Ruby) can exercise client-side behavior against one shared implementation, published as a Docker image and booted by each SDK's CI.
Why it looks the way it does
- Stateless. All behavior is selected by a single per-request
X-Lk-Mockheader (a JSON object), so the server holds no mutable state and tests run in parallel. - Multi-port = multi-region. The process binds one listener per simulated
region (
--ports). A port's position in the list is its region index; index0is the primary the SDK is initially pointed at.GET /settings/regionsadvertises all of them in order. - One header drives every attempt. The SDK sends the same control header on
the initial request and every failover retry. Each listener decides what to
do from its own index, so a single
X-Lk-Mock: {"failRegions":[0]}makes the primary fail while the first fallback succeeds — no coordination needed. - Realistic latency. Methods that block in the real server block here too:
CreateSIPParticipantwithwait_until_answeredandTransferSIPParticipanttake ~11s before responding, so SDKs can exercise their timeouts. - The whole API is mocked with populated responses. Every RoomService,
Egress, Ingress, SIP, and Connector method returns a type-correct, populated
response: scalar fields that share a name with the request are echoed (e.g.
name,metadata,identity, timeouts),id/sidfields get placeholder values, and list endpoints return one element. Both protobuf and JSON Twirp clients are supported. A client can override the response entirely with theresponsefield (see below). Unregistered/future methods fall back to an empty (all-default) message, which still decodes cleanly.
Running
go run ./cmd/test-server # primary :9999, regions :10000-10002
go run ./cmd/test-server --ports 9999,10000 # primary + one fallback
# Docker
docker build -f cmd/test-server/Dockerfile -t livekit/test-server .
docker run -p 9999-10002:9999-10002 livekit/test-server
| Flag | Env | Default | Meaning |
|---|---|---|---|
--ports |
LK_TEST_SERVER_PORTS |
9999,10000,10001,10002 |
listener ports; index = position |
--advertise-host |
LK_TEST_SERVER_ADVERTISE_HOST |
http://127.0.0.1 |
base URL used in /settings/regions |
--bind |
LK_TEST_SERVER_BIND |
0.0.0.0 |
bind address |
--twirp-prefix |
LK_TEST_SERVER_TWIRP_PREFIX |
/twirp |
Twirp path prefix |
Control protocol
All behavior is driven by a single X-Lk-Mock request header whose value is a
JSON object. The SDK sends the same header on API calls, on the
/settings/regions fetch, and on every failover retry (it must forward
client-configured custom headers onto all of them). Omit the header — or any
field — for normal behavior. Every field is optional:
| Field | Default | Effect |
|---|---|---|
failRegions |
— | array of region indices that fail this request, e.g. [0] or [0,1]. Each listener fails only if its own index is listed. |
failMode |
status |
how a failing region fails: status (write a Twirp error), or drop (close the connection → transport error). |
failStatus |
503 |
HTTP status for a status-mode failure. |
failTwirpCode |
derived from status | Twirp error code string in the failure body. |
delayMs |
— | delay (ms) before responding, on success or failure. Overrides a method's natural latency — use it for timeout tests, or set it to skip a SIP method's built-in ~11s wait. |
regionsStatus |
200 |
override the status of GET /settings/regions. |
response |
— | the response message for the called method (a JSON object, protojson-shaped); replaces the populated default, giving full control over the returned payload. |
skipAuth |
false |
true disables permission enforcement for the request (use for tests that aren't about authz, e.g. failover tests with a placeholder token). |
sipStatus |
— | fail a SIP dial method (CreateSIPParticipant/TransferSIPParticipant) with a SIP status, e.g. {"code":486,"status":"Busy Here"} (status optional). The Twirp error code and sip_status_code/sip_status/error_details metadata are derived from it exactly as the real server does. Composes with delayMs to simulate "ring, then fail". |
Example: X-Lk-Mock: {"skipAuth":true,"failRegions":[0],"failStatus":400}
Deprecated: the older per-setting headers —
X-Lk-Mock-Fail-Regions,X-Lk-Mock-Fail-Mode(incl. thedelaymode),X-Lk-Mock-Fail-Status,X-Lk-Mock-Fail-Twirp-Code,X-Lk-Mock-Delay-Ms,X-Lk-Mock-Regions-Status,X-Lk-Mock-Response,X-Lk-Mock-Skip-Auth— are still honored for existing clients and will be removed later. WhenX-Lk-Mockis also present, its fields take precedence per-field. New clients should useX-Lk-Mockonly.
Response headers:
| Header | Meaning |
|---|---|
X-Lk-Mock-Region |
index of the region that served the response (blank on a failed region). Assert on this to confirm which region a failover landed on. |
Permission enforcement
Every API method requires the same token grants the real LiveKit server checks
(see pkg/service/auth.go), so the mock doubles as a conformance check that an
SDK attaches the right permissions automatically. Tokens are parsed and verified
with the protocol's own auth helpers — the same code path the real server uses
— against the mock's configured API secret (default secret, matching
livekit-server --dev; override with --api-secret / LK_TEST_SERVER_API_SECRET).
- Missing, malformed, or wrongly-signed
Authorization→401 unauthenticated. - Validly-signed token without the required grant →
403 permission_denied. roomAdmin-scoped methods also require the token'sroomto match the request's room;ForwardParticipant/MoveParticipantadditionally requiredestinationRoomto match.
SDKs exercising permissions should sign tokens with the same API secret the mock
is configured with (secret by default).
| Grant | Methods |
|---|---|
video.roomCreate |
CreateRoom, DeleteRoom, all Connector calls |
video.roomList |
ListRooms |
video.roomRecord |
all Egress methods |
video.ingressAdmin |
all Ingress methods |
video.roomAdmin (+ room) |
room participant/data/metadata methods, AgentDispatchService methods |
video.roomAdmin (+ room + destinationRoom) |
ForwardParticipant, MoveParticipant |
sip.admin |
SIP trunk & dispatch-rule CRUD |
sip.call |
CreateSIPParticipant; TransferSIPParticipant (also needs roomAdmin) |
Send X-Lk-Mock: {"skipAuth":true} to bypass enforcement for tests that aren't
about permissions.
Common recipes
| Goal | X-Lk-Mock value |
|---|---|
| Happy path | (no header) — valid token with the method's grant → 200 from region 0 |
| Bypass auth (failover tests) | {"skipAuth":true} |
| Missing-permission error | (no header) — token without the required grant → 403 |
| Failover succeeds on region 1 | {"failRegions":[0]} |
| Exhaust to region 2 | {"failRegions":[0,1]} |
| All regions down | {"failRegions":[0,1,2,3]} |
| 4xx, no retry | {"failRegions":[0],"failStatus":400} |
| Transport-error failover | {"failRegions":[0],"failMode":"drop"} |
| Timeout test | {"delayMs":30000} |
| Region discovery unreachable | {"regionsStatus":500} |
| Custom response payload | {"response":{"sid":"RM_x","name":"my-room"}} |
| SIP busy signal | {"sipStatus":{"code":486,"status":"Busy Here"}} |
| SIP carrier decline | {"sipStatus":{"code":603}} |
Note: SDK region failover normally only engages for *.livekit.cloud hosts.
Since tests point at 127.0.0.1, set the SDK's failover-enable option to its
forced-on value so failover engages against localhost.