- Refactor the Go proxy for dependency injection: every proxy server, the bootstrap, the signal handler, the load balancers, and AMF0 now accept functional-option seams (factories/closures) so tests can inject fakes without binding real sockets, talking to real Redis, or racing on package globals. - Drop the package-global `lb.SrsLoadBalancer`. The bootstrap creates the LB locally and threads it through every proxy server constructor. Two old global indirections in `internal/signal` and `internal/rtmp/amf0` are likewise replaced by per-instance fields. - Rename `internal/server` → `internal/proxy` and rename the `lb` public surface for clarity: `SRSLoadBalancer` is split into `OriginService` / `HLSService` / `RTCService` and recomposed as `OriginLoadBalancer`; `SRSServer` → `OriginServer`; all proxy server types gain a `Proxy` qualifier (e.g. `RTMPServer` → `RTMPProxyServer`). - Extract the Redis client behind a new `internal/redisclient` package with a minimal `RedisClient` interface and a counterfeiter fake. - Add counterfeiter fakes (`proxyfakes`, `lbfakes`, `redisclientfakes`) and ~7.5k lines of unit tests covering bootstrap, memory + Redis LBs, all five proxy servers, the signal handler, and AMF0. - Add two new E2E flows — `proxy-e2e-srt-test.sh` (SRT publish through proxy, verify SRT/RTMP/HTTP-FLV/HLS playback) and `proxy-e2e-whip-test.sh` (WHIP publish, verify RTMP/HTTP-FLV/HLS via origin `rtc_to_rtmp`) — plus `setup-ffmpeg-with-whip.sh`, a macOS builder for an ffmpeg with openssl-DTLS WHIP and SRT support that the two scripts auto-invoke when needed. - Workspace reorg: move `memory/` and `skills/` to the repo root so all agent tools (Claude / Codex / Kiro / OpenClaw) share one source of truth via symlinks. Sync `docs/proxy/proxy-load-balancer.md` and `memory/srs-codebase-map.md` with the new names. No protocol, log, HTTP API, or wire-format changes. Refactor only — all externally observable proxy behavior is unchanged. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
142 lines
4.0 KiB
Go
142 lines
4.0 KiB
Go
// Copyright (c) 2026 Winlin
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
package lb
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestOriginServerID(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
name string
|
|
v *OriginServer
|
|
want string
|
|
}{
|
|
{"populated", &OriginServer{ServerID: "srv", ServiceID: "svc", PID: "1234"}, "srv-svc-1234"},
|
|
{"empty", &OriginServer{}, "--"},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := tt.v.ID(); got != tt.want {
|
|
t.Fatalf("ID()=%q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOriginServerString(t *testing.T) {
|
|
// String() routes through Format with the %v default branch.
|
|
v := &OriginServer{IP: "1.2.3.4", ServerID: "srv", ServiceID: "svc", PID: "p"}
|
|
got := v.String()
|
|
if want := "SRS ip=1.2.3.4, id=srv-svc-p"; got != want {
|
|
t.Fatalf("String()=%q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestOriginServerFormat_ShortVerbs(t *testing.T) {
|
|
v := &OriginServer{IP: "10.0.0.1", ServerID: "srv", ServiceID: "svc", PID: "9"}
|
|
want := "SRS ip=10.0.0.1, id=srv-svc-9"
|
|
for _, verb := range []string{"%v", "%s"} {
|
|
got := fmt.Sprintf(verb, v)
|
|
if got != want {
|
|
t.Fatalf("Sprintf(%q)=%q, want %q", verb, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOriginServerFormat_PlusVerbsAllFields(t *testing.T) {
|
|
ts := time.Date(2026, 5, 16, 10, 30, 45, 123_000_000, time.UTC)
|
|
v := &OriginServer{
|
|
IP: "10.0.0.1", DeviceID: "dev1",
|
|
ServerID: "srv", ServiceID: "svc", PID: "9",
|
|
RTMP: []string{":1935", ":1936"},
|
|
HTTP: []string{":8080"},
|
|
API: []string{":1985"},
|
|
SRT: []string{":10080"},
|
|
RTC: []string{":8000"},
|
|
UpdatedAt: ts,
|
|
}
|
|
|
|
for _, verb := range []string{"%+v", "%+s"} {
|
|
got := fmt.Sprintf(verb, v)
|
|
for _, sub := range []string{
|
|
"SRS ip=10.0.0.1",
|
|
"id=srv-svc-9",
|
|
"pid=9, server=srv, service=svc",
|
|
"device=dev1",
|
|
"rtmp=[:1935,:1936]",
|
|
"http=[:8080]",
|
|
"api=[:1985]",
|
|
"srt=[:10080]",
|
|
"rtc=[:8000]",
|
|
"update=2026-05-16 10:30:45.123",
|
|
} {
|
|
if !strings.Contains(got, sub) {
|
|
t.Fatalf("Sprintf(%q)=%q missing %q", verb, got, sub)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOriginServerFormat_PlusVerbMinimal(t *testing.T) {
|
|
// Plus verb with no optional fields populated exercises the false
|
|
// branches of every "if len(X) > 0 / X != \"\"" guard in Format.
|
|
v := &OriginServer{ServerID: "srv", ServiceID: "svc", PID: "9"}
|
|
got := fmt.Sprintf("%+v", v)
|
|
|
|
if !strings.Contains(got, "pid=9, server=srv, service=svc") {
|
|
t.Fatalf("%%+v output %q missing core ids", got)
|
|
}
|
|
if !strings.Contains(got, "update=") {
|
|
t.Fatalf("%%+v output %q missing update timestamp", got)
|
|
}
|
|
for _, sub := range []string{"device=", "rtmp=", "http=", "api=", "srt=", "rtc="} {
|
|
if strings.Contains(got, sub) {
|
|
t.Fatalf("%%+v output %q should not contain %q for an empty field", got, sub)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOriginServerFormat_OtherVerb(t *testing.T) {
|
|
// A non-v/s verb falls through to the default branch, which recursively
|
|
// formats with %v and appends ", fmt=%<verb>".
|
|
v := &OriginServer{IP: "1.2.3.4", ServerID: "srv", ServiceID: "svc", PID: "p"}
|
|
got := fmt.Sprintf("%d", v)
|
|
want := "SRS ip=1.2.3.4, id=srv-svc-p, fmt=%d"
|
|
if got != want {
|
|
t.Fatalf("%%d output %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestNewOriginServer(t *testing.T) {
|
|
t.Run("no opts", func(t *testing.T) {
|
|
v := NewOriginServer()
|
|
if v == nil {
|
|
t.Fatal("NewOriginServer() returned nil")
|
|
}
|
|
if v.IP != "" || v.DeviceID != "" || v.ServerID != "" || v.ServiceID != "" || v.PID != "" {
|
|
t.Fatalf("expected zero value, got %+v", v)
|
|
}
|
|
if len(v.RTMP)+len(v.HTTP)+len(v.API)+len(v.SRT)+len(v.RTC) != 0 {
|
|
t.Fatalf("expected empty endpoints, got %+v", v)
|
|
}
|
|
if !v.UpdatedAt.IsZero() {
|
|
t.Fatalf("expected zero UpdatedAt, got %v", v.UpdatedAt)
|
|
}
|
|
})
|
|
|
|
t.Run("with opts", func(t *testing.T) {
|
|
v := NewOriginServer(
|
|
func(s *OriginServer) { s.IP = "9.9.9.9" },
|
|
func(s *OriginServer) { s.ServerID = "abc" },
|
|
func(s *OriginServer) { s.RTMP = []string{":1935"} },
|
|
)
|
|
if v.IP != "9.9.9.9" || v.ServerID != "abc" || len(v.RTMP) != 1 || v.RTMP[0] != ":1935" {
|
|
t.Fatalf("opts not applied: got %+v", v)
|
|
}
|
|
})
|
|
}
|