orama/pkg/sfu/room_test.go
anonpenguin23 714a986a78 Bump version to 0.112.2 and update TURN server configuration
- Updated version in Makefile to 0.112.2.
- Enhanced SFU server error handling to ignore http.ErrServerClosed.
- Added TURNS (TURN over TLS) configuration options in TURN server and related components.
- Updated firewall rules to include TURNS ports and modified related tests.
- Implemented self-signed certificate generation for TURNS.
- Adjusted TURN server to support both UDP and TCP listeners.
- Updated WebRTC and SFU components to accommodate new TURNS configurations.
2026-02-23 16:32:32 +02:00

373 lines
9.3 KiB
Go

package sfu
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"go.uber.org/zap"
)
func testConfig() *Config {
return &Config{
ListenAddr: "10.0.0.1:8443",
Namespace: "test-ns",
MediaPortStart: 20000,
MediaPortEnd: 20500,
TURNServers: []TURNServerConfig{{Host: "1.2.3.4", Port: 3478}},
TURNSecret: "test-secret-key-32bytes-long!!!!",
TURNCredentialTTL: 600,
RQLiteDSN: "http://10.0.0.1:4001",
}
}
func testLogger() *zap.Logger {
return zap.NewNop()
}
// --- RoomManager tests ---
func TestNewRoomManager(t *testing.T) {
rm := NewRoomManager(testConfig(), testLogger())
if rm == nil {
t.Fatal("NewRoomManager returned nil")
}
if rm.RoomCount() != 0 {
t.Errorf("RoomCount = %d, want 0", rm.RoomCount())
}
}
func TestRoomManagerGetOrCreateRoom(t *testing.T) {
rm := NewRoomManager(testConfig(), testLogger())
room1 := rm.GetOrCreateRoom("room-1")
if room1 == nil {
t.Fatal("GetOrCreateRoom returned nil")
}
if room1.ID != "room-1" {
t.Errorf("Room.ID = %q, want %q", room1.ID, "room-1")
}
if room1.Namespace != "test-ns" {
t.Errorf("Room.Namespace = %q, want %q", room1.Namespace, "test-ns")
}
if rm.RoomCount() != 1 {
t.Errorf("RoomCount = %d, want 1", rm.RoomCount())
}
// Getting same room returns same instance
room1Again := rm.GetOrCreateRoom("room-1")
if room1 != room1Again {
t.Error("expected same room instance")
}
if rm.RoomCount() != 1 {
t.Errorf("RoomCount = %d, want 1 (same room)", rm.RoomCount())
}
// Different room creates new instance
room2 := rm.GetOrCreateRoom("room-2")
if room2 == nil {
t.Fatal("second room is nil")
}
if room2.ID != "room-2" {
t.Errorf("Room.ID = %q, want %q", room2.ID, "room-2")
}
if rm.RoomCount() != 2 {
t.Errorf("RoomCount = %d, want 2", rm.RoomCount())
}
}
func TestRoomManagerGetRoom(t *testing.T) {
rm := NewRoomManager(testConfig(), testLogger())
// Non-existent room returns nil
room := rm.GetRoom("nonexistent")
if room != nil {
t.Error("expected nil for non-existent room")
}
// Create a room and retrieve it
rm.GetOrCreateRoom("room-1")
room = rm.GetRoom("room-1")
if room == nil {
t.Fatal("expected non-nil for existing room")
}
if room.ID != "room-1" {
t.Errorf("Room.ID = %q, want %q", room.ID, "room-1")
}
}
func TestRoomManagerCloseAll(t *testing.T) {
rm := NewRoomManager(testConfig(), testLogger())
rm.GetOrCreateRoom("room-1")
rm.GetOrCreateRoom("room-2")
rm.GetOrCreateRoom("room-3")
if rm.RoomCount() != 3 {
t.Fatalf("RoomCount = %d, want 3", rm.RoomCount())
}
rm.CloseAll()
if rm.RoomCount() != 0 {
t.Errorf("RoomCount after CloseAll = %d, want 0", rm.RoomCount())
}
}
func TestRoomManagerGetOrCreateRoomReplacesClosedRoom(t *testing.T) {
rm := NewRoomManager(testConfig(), testLogger())
room1 := rm.GetOrCreateRoom("room-1")
room1.Close()
// Getting the same room ID after close should create a new room
room1New := rm.GetOrCreateRoom("room-1")
if room1New == room1 {
t.Error("expected new room instance after close")
}
if room1New.IsClosed() {
t.Error("new room should not be closed")
}
}
// --- Room tests ---
func TestRoomIsClosed(t *testing.T) {
rm := NewRoomManager(testConfig(), testLogger())
room := rm.GetOrCreateRoom("room-1")
if room.IsClosed() {
t.Error("new room should not be closed")
}
room.Close()
if !room.IsClosed() {
t.Error("room should be closed after Close()")
}
}
func TestRoomCloseIdempotent(t *testing.T) {
rm := NewRoomManager(testConfig(), testLogger())
room := rm.GetOrCreateRoom("room-1")
// Should not panic or error when called multiple times
if err := room.Close(); err != nil {
t.Errorf("first Close() returned error: %v", err)
}
if err := room.Close(); err != nil {
t.Errorf("second Close() returned error: %v", err)
}
}
func TestRoomGetParticipantsEmpty(t *testing.T) {
rm := NewRoomManager(testConfig(), testLogger())
room := rm.GetOrCreateRoom("room-1")
participants := room.GetParticipants()
if len(participants) != 0 {
t.Errorf("Participants count = %d, want 0", len(participants))
}
if room.GetParticipantCount() != 0 {
t.Errorf("ParticipantCount = %d, want 0", room.GetParticipantCount())
}
}
func TestRoomBuildICEServers(t *testing.T) {
rm := NewRoomManager(testConfig(), testLogger())
room := rm.GetOrCreateRoom("room-1")
servers := room.buildICEServers()
if len(servers) != 1 {
t.Fatalf("ICE servers count = %d, want 1", len(servers))
}
if len(servers[0].URLs) != 2 {
t.Fatalf("URLs count = %d, want 2", len(servers[0].URLs))
}
if servers[0].URLs[0] != "turn:1.2.3.4:3478?transport=udp" {
t.Errorf("URL[0] = %q, want %q", servers[0].URLs[0], "turn:1.2.3.4:3478?transport=udp")
}
if servers[0].URLs[1] != "turn:1.2.3.4:3478?transport=tcp" {
t.Errorf("URL[1] = %q, want %q", servers[0].URLs[1], "turn:1.2.3.4:3478?transport=tcp")
}
if servers[0].Username == "" {
t.Error("Username should not be empty")
}
if servers[0].Credential == "" {
t.Error("Credential should not be empty")
}
}
func TestRoomBuildICEServersNoTURN(t *testing.T) {
cfg := testConfig()
cfg.TURNServers = nil
rm := NewRoomManager(cfg, testLogger())
room := rm.GetOrCreateRoom("room-1")
servers := room.buildICEServers()
if servers != nil {
t.Errorf("expected nil ICE servers when no TURN configured, got %v", servers)
}
}
func TestRoomBuildICEServersNoSecret(t *testing.T) {
cfg := testConfig()
cfg.TURNSecret = ""
rm := NewRoomManager(cfg, testLogger())
room := rm.GetOrCreateRoom("room-1")
servers := room.buildICEServers()
if servers != nil {
t.Errorf("expected nil ICE servers when no secret, got %v", servers)
}
}
func TestRoomBuildICEServersMultipleTURN(t *testing.T) {
cfg := testConfig()
cfg.TURNServers = []TURNServerConfig{
{Host: "1.2.3.4", Port: 3478}, // non-secure → UDP + TCP = 2 URIs
{Host: "5.6.7.8", Port: 5349, Secure: true}, // secure → 1 URI
}
rm := NewRoomManager(cfg, testLogger())
room := rm.GetOrCreateRoom("room-1")
servers := room.buildICEServers()
if len(servers) != 1 {
t.Fatalf("ICE servers count = %d, want 1", len(servers))
}
// 1 non-secure (UDP+TCP) + 1 secure (TURNS) = 3 URIs
if len(servers[0].URLs) != 3 {
t.Fatalf("URLs count = %d, want 3", len(servers[0].URLs))
}
}
// --- Empty room cleanup test ---
func TestEmptyRoomCleanup(t *testing.T) {
// Override timeAfter for instant timer
origTimeAfter := timeAfter
timeAfter = func(d time.Duration) <-chan time.Time {
ch := make(chan time.Time, 1)
ch <- time.Now()
return ch
}
defer func() { timeAfter = origTimeAfter }()
rm := NewRoomManager(testConfig(), testLogger())
room := rm.GetOrCreateRoom("room-1")
// Trigger the onEmpty callback (which starts cleanup timer)
room.onEmpty(room)
// Give the goroutine time to execute
time.Sleep(50 * time.Millisecond)
if rm.RoomCount() != 0 {
t.Errorf("RoomCount = %d, want 0 (should have been cleaned up)", rm.RoomCount())
}
}
// --- Server health tests ---
func TestHealthEndpointOK(t *testing.T) {
cfg := testConfig()
server, err := NewServer(cfg, testLogger())
if err != nil {
t.Fatalf("NewServer failed: %v", err)
}
req := httptest.NewRequest("GET", "/health", nil)
w := httptest.NewRecorder()
server.handleHealth(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if body != `{"status":"ok","rooms":0}` {
t.Errorf("body = %q, want %q", body, `{"status":"ok","rooms":0}`)
}
}
func TestHealthEndpointDraining(t *testing.T) {
cfg := testConfig()
server, err := NewServer(cfg, testLogger())
if err != nil {
t.Fatalf("NewServer failed: %v", err)
}
// Set draining
server.drainingMu.Lock()
server.draining = true
server.drainingMu.Unlock()
req := httptest.NewRequest("GET", "/health", nil)
w := httptest.NewRecorder()
server.handleHealth(w, req)
if w.Code != http.StatusServiceUnavailable {
t.Errorf("status = %d, want %d", w.Code, http.StatusServiceUnavailable)
}
body := w.Body.String()
if body != `{"status":"draining","rooms":0}` {
t.Errorf("body = %q, want %q", body, `{"status":"draining","rooms":0}`)
}
}
func TestServerDrainSetsFlag(t *testing.T) {
// Override timeAfter for instant timer
origTimeAfter := timeAfter
timeAfter = func(d time.Duration) <-chan time.Time {
ch := make(chan time.Time, 1)
ch <- time.Now()
return ch
}
defer func() { timeAfter = origTimeAfter }()
cfg := testConfig()
server, err := NewServer(cfg, testLogger())
if err != nil {
t.Fatalf("NewServer failed: %v", err)
}
server.Drain(0)
server.drainingMu.RLock()
draining := server.draining
server.drainingMu.RUnlock()
if !draining {
t.Error("expected draining to be true after Drain()")
}
}
func TestServerNewServerValidation(t *testing.T) {
// Invalid config should return error
cfg := &Config{} // Empty = invalid
_, err := NewServer(cfg, testLogger())
if err == nil {
t.Error("expected error for invalid config")
}
}
func TestServerSignalEndpointRejectsDraining(t *testing.T) {
cfg := testConfig()
server, err := NewServer(cfg, testLogger())
if err != nil {
t.Fatalf("NewServer failed: %v", err)
}
server.drainingMu.Lock()
server.draining = true
server.drainingMu.Unlock()
req := httptest.NewRequest("GET", "/ws/signal", nil)
w := httptest.NewRecorder()
server.handleSignal(w, req)
if w.Code != http.StatusServiceUnavailable {
t.Errorf("status = %d, want %d", w.Code, http.StatusServiceUnavailable)
}
}