wraith/internal/rdp/service_test.go
Vantz Stockwell 6c65f171f8 feat: RDP service with mock backend for development
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>
2026-03-17 07:17:26 -04:00

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")
}
}