add mock for testing region pins with API (#4691)

we'll ensure that clients support redirection when making an API
to an unpinned region
This commit is contained in:
David Zhao
2026-07-19 21:12:35 -07:00
committed by GitHub
parent 366cadcd96
commit 3f59d0dd9e
4 changed files with 71 additions and 1 deletions
+1
View File
@@ -67,6 +67,7 @@ field — for normal behavior. Every field is optional:
| `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". |
| `pinnedRegions` | — | array of region **names** the project is pinned to (its **allowed** regions), e.g. `["region-1"]`, mirroring cloud's per-project `PinnedRegions` (region names, not indices). When non-empty a pin is in effect: any region whose name is **not** listed rejects the request with **HTTP 451** (a region-pin violation), and `GET /settings/regions` returns **only** the listed regions. The SDK is expected to see the 451, re-fetch `/settings/regions`, and retry against an allowed region — the 451 carries no region hint (its body is the middleware's plain text, not a Twirp error). This redirect is always active in clients; it can't be disabled. Use a name no listener advertises (e.g. `["region-99"]`) to model a pinned region that's unreachable. Names are the mock's region names (`region-0`, `region-1`, …); contrast `failRegions`, which addresses listeners by index. |
Example: `X-Lk-Mock: {"skipAuth":true,"failRegions":[0],"failStatus":400}`
+22
View File
@@ -30,6 +30,16 @@ const (
headerRegion = "X-Lk-Mock-Region"
)
const (
// regionPinStatus is the HTTP status cloud middleware returns for a region-pin
// violation — an API call reaching a region the project is not pinned to. It is
// 451 (Unavailable For Legal Reasons); clients key off it to rediscover regions
// and retry against an allowed one.
regionPinStatus = http.StatusUnavailableForLegalReasons
// regionPinMessage is the plain-text body the middleware writes with the 451.
regionPinMessage = "project not allowed in this region."
)
// Deprecated: the individual X-Lk-Mock-* control headers predate the unified
// X-Lk-Mock JSON header. They are still honored for existing clients; new
// clients should send X-Lk-Mock instead. When X-Lk-Mock is present its fields
@@ -86,6 +96,18 @@ type mockConfig struct {
// 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"`
// PinnedRegions lists the region names a project is pinned to (its allowed
// regions), mirroring cloud's per-project PinnedRegions — which are region names,
// not indices. When non-empty a pin is in effect: any region whose name is NOT
// listed rejects the request with HTTP 451 (see regionPinStatus), exactly as
// cloud middleware turns away an API call reaching a region the project is not
// pinned to, and GET /settings/regions returns only the listed regions. The
// client is expected to see the 451, re-fetch /settings/regions, and retry
// against an allowed region — the 451 carries no region hint. An empty/absent
// value means "not pinned": every region serves and discovery returns all
// regions. Names are this mock's region names (e.g. "region-0"); contrast
// FailRegions, which addresses listeners by index.
PinnedRegions []string `json:"pinnedRegions,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.
+19
View File
@@ -17,6 +17,7 @@ package main
import (
"io"
"net/http"
"slices"
"strconv"
"strings"
"time"
@@ -145,6 +146,14 @@ func (h *mockHandler) serveAPI(w http.ResponseWriter, r *http.Request) {
return
}
// A project pinned to other regions is turned away by cloud middleware up front
// — before the request is served and with no latency — when it reaches a region
// it isn't pinned to. The check is by region name, as it is on the real server.
if len(cfg.PinnedRegions) > 0 && !slices.Contains(cfg.PinnedRegions, h.regionName()) {
h.failRegionPin(w)
return
}
// Delay before responding (success or failure). An explicit delayMs overrides
// the method's natural latency — e.g. CreateSIPParticipant blocking until the
// callee answers.
@@ -211,6 +220,16 @@ func (h *mockHandler) failSIP(w http.ResponseWriter, cfg *mockConfig) {
writeTwirpErr(w, xtwirp.ToError(st))
}
// failRegionPin rejects the request with HTTP 451, mirroring cloud middleware
// turning away an API call from a project pinned to a different region. The body
// is the middleware's plain-text message (not a Twirp error): clients key off the
// 451 status to rediscover regions and retry against an allowed one.
func (h *mockHandler) failRegionPin(w http.ResponseWriter) {
w.Header().Set(headerRegion, "")
w.WriteHeader(regionPinStatus)
_, _ = w.Write([]byte(regionPinMessage))
}
// 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
+29 -1
View File
@@ -112,7 +112,14 @@ func (h *mockHandler) handleRegions(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(cfg.RegionsStatus)
return
}
body, err := protojson.Marshal(h.regions)
regions := h.regions
if len(cfg.PinnedRegions) > 0 {
// A pinned project's discovery returns only its allowed regions, so the
// client redirects straight to one after a 451 — mirroring cloud filtering
// /settings/regions by the project's PinnedRegions.
regions = pinnedRegionSettings(h.regions, cfg.PinnedRegions)
}
body, err := protojson.Marshal(regions)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
@@ -127,6 +134,27 @@ func (h *mockHandler) handleTwirp(w http.ResponseWriter, r *http.Request) {
h.serveAPI(w, r)
}
// regionName is the advertised name of the region this listener represents.
func (h *mockHandler) regionName() string {
if h.regionIndex < len(h.regions.Regions) {
return h.regions.Regions[h.regionIndex].Region
}
return ""
}
// pinnedRegionSettings returns only the regions whose name appears in pinned,
// mirroring how cloud filters /settings/regions to a project's PinnedRegions.
// Names outside the region list are ignored.
func pinnedRegionSettings(all *livekit.RegionSettings, pinned []string) *livekit.RegionSettings {
out := &livekit.RegionSettings{}
for _, region := range all.Regions {
if slices.Contains(pinned, region.Region) {
out.Regions = append(out.Regions, region)
}
}
return out
}
func (h *mockHandler) shouldFail(cfg *mockConfig) bool {
return slices.Contains(cfg.FailRegions, h.regionIndex)
}