added mocking function for SIP dialing methods (#4642)

This commit is contained in:
David Zhao
2026-07-04 21:47:02 +02:00
committed by GitHub
parent 00348c1299
commit 0bc73fd0da
4 changed files with 65 additions and 0 deletions
+3
View File
@@ -66,6 +66,7 @@ field — for normal behavior. Every field is optional:
| `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}`
@@ -129,6 +130,8 @@ about permissions.
| 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
+14
View File
@@ -80,12 +80,26 @@ type mockConfig struct {
// SkipAuth disables permission enforcement for this request (for tests that
// aren't about authz, e.g. failover tests with a placeholder token).
SkipAuth bool `json:"skipAuth,omitempty"`
// SIPStatus, when set on a SIP dial method (CreateSIPParticipant /
// TransferSIPParticipant), fails the call with this SIP status. The Twirp
// error code and metadata (sip_status_code, sip_status, error_details) are
// derived from it exactly as the real server does, so the SDK sees an
// identical error. Composes with DelayMs to simulate "ring, then fail".
SIPStatus *sipStatusConfig `json:"sipStatus,omitempty"`
// legacyDelayMs is the sleep used by the deprecated "delay" fail mode. It is
// populated only from the legacy X-Lk-Mock-Delay-Ms header, never from JSON.
legacyDelayMs int
}
// sipStatusConfig is a SIP response to inject; see mockConfig.SIPStatus.
type sipStatusConfig struct {
// Code is the SIP response code, e.g. 486 (Busy Here) or 603 (Decline).
Code int `json:"code"`
// Status is the SIP reason phrase; defaults to the code's canonical name.
Status string `json:"status,omitempty"`
}
// parseMockConfig builds the request's config. Deprecated individual X-Lk-Mock-*
// headers form the base; the unified X-Lk-Mock JSON header (if present) is
// overlaid on top, so its fields win per-field while absent fields keep the
+28
View File
@@ -27,6 +27,7 @@ import (
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/utils/protojson"
"github.com/livekit/protocol/utils/xtwirp"
)
// apiSpec captures the request and response message types for one Twirp method,
@@ -155,6 +156,13 @@ func (h *mockHandler) serveAPI(w http.ResponseWriter, r *http.Request) {
time.Sleep(delay)
}
// A SIP dial that fails carries a SIP status; the Twirp code and metadata are
// derived from it exactly as the real server does.
if cfg.SIPStatus != nil && isSIPDialMethod(key) {
h.failSIP(w, &cfg)
return
}
if h.shouldFail(&cfg) {
h.fail(w, &cfg)
return
@@ -183,6 +191,26 @@ func methodLatency(key string, req proto.Message) time.Duration {
// mock — long enough to exercise client-side timeouts around these calls.
const sipAnswerLatency = 11 * time.Second
// isSIPDialMethod reports whether key places a call that can fail with a SIP status.
func isSIPDialMethod(key string) bool {
switch key {
case "livekit.SIP/CreateSIPParticipant", "livekit.SIP/TransferSIPParticipant":
return true
}
return false
}
// failSIP fails the request with the configured SIP status, mirroring the real
// server: the status maps to a Twirp error code and attaches sip_status_code,
// sip_status, and error_details metadata via xtwirp.
func (h *mockHandler) failSIP(w http.ResponseWriter, cfg *mockConfig) {
st := &livekit.SIPStatus{
Code: livekit.SIPStatusCode(cfg.SIPStatus.Code),
Status: cfg.SIPStatus.Status,
}
writeTwirpErr(w, xtwirp.ToError(st))
}
// writeAPIResponse serves a populated, type-correct response for a known API
// method. The response is the reflection-populated default unless the mock
// config carries a `response` (protojson), which overrides it entirely. Content
+20
View File
@@ -17,6 +17,7 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"net/http"
@@ -28,6 +29,8 @@ import (
"syscall"
"time"
"github.com/twitchtv/twirp"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/utils/protojson"
)
@@ -163,6 +166,23 @@ func writeTwirpErrorCode(w http.ResponseWriter, status int, code, msg string) {
_, _ = fmt.Fprintf(w, `{"code":%q,"msg":%q}`, code, msg)
}
// writeTwirpErr writes a full Twirp JSON error — code, message, and metadata —
// using the HTTP status Twirp derives from the error code.
func writeTwirpErr(w http.ResponseWriter, terr twirp.Error) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set(headerRegion, "")
w.WriteHeader(twirp.ServerHTTPStatusFromErrorCode(terr.Code()))
_ = json.NewEncoder(w).Encode(struct {
Code string `json:"code"`
Msg string `json:"msg"`
Meta map[string]string `json:"meta,omitempty"`
}{
Code: string(terr.Code()),
Msg: terr.Msg(),
Meta: terr.MetaMap(),
})
}
func twirpCodeForStatus(status int) string {
switch {
case status == http.StatusBadRequest: