Add RDPService with session lifecycle management (connect, disconnect, input forwarding, frame retrieval) using an injected backend factory. Implement MockBackend that generates animated test frames with a blue-purple gradient, bouncing color-cycling rectangle, pulsing circle, and grid overlay for canvas renderer verification on macOS without FreeRDP. The real FreeRDP purego bindings are deferred to Windows. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
256 lines
6.7 KiB
Go
256 lines
6.7 KiB
Go
package rdp
|
|
|
|
import "testing"
|
|
|
|
func TestNewRDPService(t *testing.T) {
|
|
svc := NewRDPService(func() RDPBackend { return NewMockBackend() })
|
|
if svc == nil {
|
|
t.Fatal("NewRDPService returned nil")
|
|
}
|
|
if len(svc.ListSessions()) != 0 {
|
|
t.Error("new service should have no sessions")
|
|
}
|
|
}
|
|
|
|
func TestConnectWithMockBackend(t *testing.T) {
|
|
svc := NewRDPService(func() RDPBackend { return NewMockBackend() })
|
|
|
|
config := RDPConfig{
|
|
Hostname: "test-host",
|
|
Port: 3389,
|
|
Username: "admin",
|
|
Width: 1024,
|
|
Height: 768,
|
|
}
|
|
|
|
sessionID, err := svc.Connect(config, 42)
|
|
if err != nil {
|
|
t.Fatalf("Connect error: %v", err)
|
|
}
|
|
if sessionID == "" {
|
|
t.Fatal("Connect returned empty session ID")
|
|
}
|
|
|
|
// Verify session is tracked
|
|
sessions := svc.ListSessions()
|
|
if len(sessions) != 1 {
|
|
t.Fatalf("expected 1 session, got %d", len(sessions))
|
|
}
|
|
if sessions[0].ID != sessionID {
|
|
t.Errorf("session ID = %q, want %q", sessions[0].ID, sessionID)
|
|
}
|
|
if sessions[0].ConnID != 42 {
|
|
t.Errorf("ConnID = %d, want 42", sessions[0].ConnID)
|
|
}
|
|
if sessions[0].Config.Hostname != "test-host" {
|
|
t.Errorf("Hostname = %q, want %q", sessions[0].Config.Hostname, "test-host")
|
|
}
|
|
}
|
|
|
|
func TestConnectDefaults(t *testing.T) {
|
|
svc := NewRDPService(func() RDPBackend { return NewMockBackend() })
|
|
|
|
// Connect with zero values — should get defaults
|
|
config := RDPConfig{Hostname: "host"}
|
|
sessionID, err := svc.Connect(config, 1)
|
|
if err != nil {
|
|
t.Fatalf("Connect error: %v", err)
|
|
}
|
|
|
|
info, err := svc.GetSessionInfo(sessionID)
|
|
if err != nil {
|
|
t.Fatalf("GetSessionInfo error: %v", err)
|
|
}
|
|
|
|
if info.Config.Port != 3389 {
|
|
t.Errorf("default Port = %d, want 3389", info.Config.Port)
|
|
}
|
|
if info.Config.Width != 1920 {
|
|
t.Errorf("default Width = %d, want 1920", info.Config.Width)
|
|
}
|
|
if info.Config.Height != 1080 {
|
|
t.Errorf("default Height = %d, want 1080", info.Config.Height)
|
|
}
|
|
if info.Config.ColorDepth != 32 {
|
|
t.Errorf("default ColorDepth = %d, want 32", info.Config.ColorDepth)
|
|
}
|
|
if info.Config.Security != "nla" {
|
|
t.Errorf("default Security = %q, want %q", info.Config.Security, "nla")
|
|
}
|
|
}
|
|
|
|
func TestDisconnect(t *testing.T) {
|
|
svc := NewRDPService(func() RDPBackend { return NewMockBackend() })
|
|
|
|
config := RDPConfig{Hostname: "host", Width: 800, Height: 600}
|
|
sessionID, err := svc.Connect(config, 1)
|
|
if err != nil {
|
|
t.Fatalf("Connect error: %v", err)
|
|
}
|
|
|
|
if err := svc.Disconnect(sessionID); err != nil {
|
|
t.Fatalf("Disconnect error: %v", err)
|
|
}
|
|
|
|
if len(svc.ListSessions()) != 0 {
|
|
t.Error("expected 0 sessions after disconnect")
|
|
}
|
|
|
|
// Verify double-disconnect returns error
|
|
if err := svc.Disconnect(sessionID); err == nil {
|
|
t.Error("expected error on double disconnect")
|
|
}
|
|
}
|
|
|
|
func TestDisconnectNotFound(t *testing.T) {
|
|
svc := NewRDPService(func() RDPBackend { return NewMockBackend() })
|
|
if err := svc.Disconnect("nonexistent"); err == nil {
|
|
t.Error("expected error for nonexistent session")
|
|
}
|
|
}
|
|
|
|
func TestSessionTracking(t *testing.T) {
|
|
svc := NewRDPService(func() RDPBackend { return NewMockBackend() })
|
|
|
|
config := RDPConfig{Hostname: "host", Width: 640, Height: 480}
|
|
|
|
id1, _ := svc.Connect(config, 1)
|
|
id2, _ := svc.Connect(config, 2)
|
|
id3, _ := svc.Connect(config, 3)
|
|
|
|
if len(svc.ListSessions()) != 3 {
|
|
t.Fatalf("expected 3 sessions, got %d", len(svc.ListSessions()))
|
|
}
|
|
|
|
// Disconnect middle session
|
|
if err := svc.Disconnect(id2); err != nil {
|
|
t.Fatalf("Disconnect error: %v", err)
|
|
}
|
|
|
|
sessions := svc.ListSessions()
|
|
if len(sessions) != 2 {
|
|
t.Fatalf("expected 2 sessions, got %d", len(sessions))
|
|
}
|
|
|
|
// Verify remaining sessions
|
|
ids := make(map[string]bool)
|
|
for _, s := range sessions {
|
|
ids[s.ID] = true
|
|
}
|
|
if !ids[id1] || !ids[id3] {
|
|
t.Error("remaining sessions should be id1 and id3")
|
|
}
|
|
}
|
|
|
|
func TestSendMouse(t *testing.T) {
|
|
svc := NewRDPService(func() RDPBackend { return NewMockBackend() })
|
|
|
|
config := RDPConfig{Hostname: "host", Width: 800, Height: 600}
|
|
sessionID, _ := svc.Connect(config, 1)
|
|
|
|
if err := svc.SendMouse(sessionID, 100, 200, MouseFlagMove); err != nil {
|
|
t.Errorf("SendMouse error: %v", err)
|
|
}
|
|
|
|
if err := svc.SendMouse(sessionID, 100, 200, MouseFlagButton1|MouseFlagDown); err != nil {
|
|
t.Errorf("SendMouse click error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSendMouseNotFound(t *testing.T) {
|
|
svc := NewRDPService(func() RDPBackend { return NewMockBackend() })
|
|
if err := svc.SendMouse("nonexistent", 0, 0, 0); err == nil {
|
|
t.Error("expected error for nonexistent session")
|
|
}
|
|
}
|
|
|
|
func TestSendKey(t *testing.T) {
|
|
svc := NewRDPService(func() RDPBackend { return NewMockBackend() })
|
|
|
|
config := RDPConfig{Hostname: "host", Width: 800, Height: 600}
|
|
sessionID, _ := svc.Connect(config, 1)
|
|
|
|
// Send key down
|
|
sc, _ := JSKeyToScancode("KeyA")
|
|
if err := svc.SendKey(sessionID, sc, true); err != nil {
|
|
t.Errorf("SendKey down error: %v", err)
|
|
}
|
|
|
|
// Send key up
|
|
if err := svc.SendKey(sessionID, sc, false); err != nil {
|
|
t.Errorf("SendKey up error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSendKeyNotFound(t *testing.T) {
|
|
svc := NewRDPService(func() RDPBackend { return NewMockBackend() })
|
|
if err := svc.SendKey("nonexistent", 0x001E, true); err == nil {
|
|
t.Error("expected error for nonexistent session")
|
|
}
|
|
}
|
|
|
|
func TestSendClipboard(t *testing.T) {
|
|
svc := NewRDPService(func() RDPBackend { return NewMockBackend() })
|
|
|
|
config := RDPConfig{Hostname: "host", Width: 800, Height: 600}
|
|
sessionID, _ := svc.Connect(config, 1)
|
|
|
|
if err := svc.SendClipboard(sessionID, "hello clipboard"); err != nil {
|
|
t.Errorf("SendClipboard error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGetFrame(t *testing.T) {
|
|
svc := NewRDPService(func() RDPBackend { return NewMockBackend() })
|
|
|
|
config := RDPConfig{Hostname: "host", Width: 100, Height: 100}
|
|
sessionID, _ := svc.Connect(config, 1)
|
|
|
|
frame, err := svc.GetFrame(sessionID)
|
|
if err != nil {
|
|
t.Fatalf("GetFrame error: %v", err)
|
|
}
|
|
|
|
expectedLen := 100 * 100 * 4
|
|
if len(frame) != expectedLen {
|
|
t.Errorf("frame length = %d, want %d", len(frame), expectedLen)
|
|
}
|
|
|
|
// Verify the frame is not all zeros (mock generates colored content)
|
|
allZero := true
|
|
for _, b := range frame {
|
|
if b != 0 {
|
|
allZero = false
|
|
break
|
|
}
|
|
}
|
|
if allZero {
|
|
t.Error("mock frame should not be all zeros")
|
|
}
|
|
}
|
|
|
|
func TestGetFrameNotFound(t *testing.T) {
|
|
svc := NewRDPService(func() RDPBackend { return NewMockBackend() })
|
|
_, err := svc.GetFrame("nonexistent")
|
|
if err == nil {
|
|
t.Error("expected error for nonexistent session")
|
|
}
|
|
}
|
|
|
|
func TestIsConnected(t *testing.T) {
|
|
svc := NewRDPService(func() RDPBackend { return NewMockBackend() })
|
|
|
|
config := RDPConfig{Hostname: "host", Width: 800, Height: 600}
|
|
sessionID, _ := svc.Connect(config, 1)
|
|
|
|
if !svc.IsConnected(sessionID) {
|
|
t.Error("session should be connected")
|
|
}
|
|
|
|
svc.Disconnect(sessionID)
|
|
|
|
if svc.IsConnected(sessionID) {
|
|
t.Error("session should not be connected after disconnect")
|
|
}
|
|
}
|