From 0bc73fd0daf5b312bdc06706fd391efd6018931e Mon Sep 17 00:00:00 2001 From: David Zhao Date: Sat, 4 Jul 2026 21:47:02 +0200 Subject: [PATCH] added mocking function for SIP dialing methods (#4642) --- cmd/test-server/README.md | 3 +++ cmd/test-server/config.go | 14 ++++++++++++++ cmd/test-server/handlers.go | 28 ++++++++++++++++++++++++++++ cmd/test-server/main.go | 20 ++++++++++++++++++++ 4 files changed, 65 insertions(+) diff --git a/cmd/test-server/README.md b/cmd/test-server/README.md index 02991a934..2f13f00da 100644 --- a/cmd/test-server/README.md +++ b/cmd/test-server/README.md @@ -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 diff --git a/cmd/test-server/config.go b/cmd/test-server/config.go index 2e1aefe40..bca4ee251 100644 --- a/cmd/test-server/config.go +++ b/cmd/test-server/config.go @@ -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 diff --git a/cmd/test-server/handlers.go b/cmd/test-server/handlers.go index 7878b7856..9d14fd2b3 100644 --- a/cmd/test-server/handlers.go +++ b/cmd/test-server/handlers.go @@ -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 diff --git a/cmd/test-server/main.go b/cmd/test-server/main.go index 5d54885b6..7850a4c7c 100644 --- a/cmd/test-server/main.go +++ b/cmd/test-server/main.go @@ -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: