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) != 1 { t.Fatalf("URLs count = %d, want 1", len(servers[0].URLs)) } if servers[0].URLs[0] != "turn:1.2.3.4:3478?transport=udp" { t.Errorf("URL = %q, want %q", servers[0].URLs[0], "turn:1.2.3.4:3478?transport=udp") } 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}, {Host: "5.6.7.8", Port: 443}, } 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)) } if len(servers[0].URLs) != 2 { t.Fatalf("URLs count = %d, want 2", 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) } }