package onvif import ( "context" "errors" "net/http" "net/http/httptest" "strings" "testing" ) const testDeviceIOXMLHeader = `` func newMockDeviceIOServer() *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/soap+xml") body := make([]byte, r.ContentLength) _, _ = r.Body.Read(body) bodyStr := string(body) var response string switch { case strings.Contains(bodyStr, "GetServiceCapabilities") && strings.Contains(bodyStr, "deviceIO"): response = testDeviceIOXMLHeader + ` ` case strings.Contains(bodyStr, "GetDigitalInputConfigurationOptions"): response = testDeviceIOXMLHeader + ` open closed ` case strings.Contains(bodyStr, "GetDigitalInputs"): response = testDeviceIOXMLHeader + ` ` case strings.Contains(bodyStr, "SetDigitalInputConfigurations"): response = testDeviceIOXMLHeader + ` ` case strings.Contains(bodyStr, "GetVideoOutputs"): response = testDeviceIOXMLHeader + ` 1920 1080 60.0 16:9 ` case strings.Contains(bodyStr, "GetSerialPortConfigurationOptions"): response = testDeviceIOXMLHeader + ` 96001920038400 NoneOddEven 78 12 ` case strings.Contains(bodyStr, "GetSerialPortConfiguration"): response = testDeviceIOXMLHeader + ` RS232 9600 None 8 1 ` case strings.Contains(bodyStr, "GetSerialPorts"): response = testDeviceIOXMLHeader + ` RS232 RS485 ` case strings.Contains(bodyStr, "SetSerialPortConfiguration"): response = testDeviceIOXMLHeader + ` ` case strings.Contains(bodyStr, "SendReceiveSerialCommand"): response = testDeviceIOXMLHeader + ` OK ` case strings.Contains(bodyStr, "GetVideoOutputConfigurationOptions"): response = testDeviceIOXMLHeader + ` video_out_001 video_out_002 ` case strings.Contains(bodyStr, "GetVideoOutputConfiguration"): response = testDeviceIOXMLHeader + ` Main Output 2 video_out_001 ` case strings.Contains(bodyStr, "SetVideoOutputConfiguration"): response = testDeviceIOXMLHeader + ` ` case strings.Contains(bodyStr, "GetRelayOutputOptions"): response = testDeviceIOXMLHeader + ` Monostable Bistable PT1S PT5S PT10S true ` default: response = testDeviceIOXMLHeader + ` SOAP-ENV:Receiver Unknown action ` } _, _ = w.Write([]byte(response)) })) } func TestGetDeviceIOServiceCapabilities(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() caps, err := client.GetDeviceIOServiceCapabilities(ctx) if err != nil { t.Fatalf("GetDeviceIOServiceCapabilities failed: %v", err) } if caps.VideoSources != 4 { t.Errorf("Expected VideoSources to be 4, got %d", caps.VideoSources) } if caps.VideoOutputs != 2 { t.Errorf("Expected VideoOutputs to be 2, got %d", caps.VideoOutputs) } if caps.AudioSources != 2 { t.Errorf("Expected AudioSources to be 2, got %d", caps.AudioSources) } if caps.AudioOutputs != 2 { t.Errorf("Expected AudioOutputs to be 2, got %d", caps.AudioOutputs) } if caps.RelayOutputs != 4 { t.Errorf("Expected RelayOutputs to be 4, got %d", caps.RelayOutputs) } if caps.SerialPorts != 2 { t.Errorf("Expected SerialPorts to be 2, got %d", caps.SerialPorts) } if caps.DigitalInputs != 8 { t.Errorf("Expected DigitalInputs to be 8, got %d", caps.DigitalInputs) } if !caps.DigitalInputOptions { t.Error("Expected DigitalInputOptions to be true") } if !caps.SerialPortConfiguration { t.Error("Expected SerialPortConfiguration to be true") } } func TestGetDigitalInputs(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() inputs, err := client.GetDigitalInputs(ctx) if err != nil { t.Fatalf("GetDigitalInputs failed: %v", err) } if len(inputs) != 2 { t.Fatalf("Expected 2 digital inputs, got %d", len(inputs)) } if inputs[0].Token != "input_001" { t.Errorf("Expected first input token 'input_001', got '%s'", inputs[0].Token) } if inputs[0].IdleState != DigitalIdleOpen { t.Errorf("Expected first input idle state 'open', got '%s'", inputs[0].IdleState) } if inputs[1].IdleState != DigitalIdleClosed { t.Errorf("Expected second input idle state 'closed', got '%s'", inputs[1].IdleState) } } func TestGetDigitalInputConfigurationOptions(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() options, err := client.GetDigitalInputConfigurationOptions(ctx, "input_001") if err != nil { t.Fatalf("GetDigitalInputConfigurationOptions failed: %v", err) } if len(options.IdleStateOptions) != 2 { t.Errorf("Expected 2 idle state options, got %d", len(options.IdleStateOptions)) } } func TestGetDigitalInputConfigurationOptionsInvalidToken(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() _, err = client.GetDigitalInputConfigurationOptions(ctx, "") if !errors.Is(err, ErrInvalidDigitalInputToken) { t.Errorf("Expected ErrInvalidDigitalInputToken, got %v", err) } } func TestSetDigitalInputConfigurations(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() inputs := []*DigitalInput{ {Token: "input_001", IdleState: DigitalIdleOpen}, {Token: "input_002", IdleState: DigitalIdleClosed}, } err = client.SetDigitalInputConfigurations(ctx, inputs) if err != nil { t.Fatalf("SetDigitalInputConfigurations failed: %v", err) } } func TestSetDigitalInputConfigurationsValidation(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() // Test empty inputs. err = client.SetDigitalInputConfigurations(ctx, []*DigitalInput{}) if !errors.Is(err, ErrDigitalInputConfigNil) { t.Errorf("Expected ErrDigitalInputConfigNil, got %v", err) } // Test input with empty token. inputs := []*DigitalInput{{Token: "", IdleState: DigitalIdleOpen}} err = client.SetDigitalInputConfigurations(ctx, inputs) if !errors.Is(err, ErrInvalidDigitalInputToken) { t.Errorf("Expected ErrInvalidDigitalInputToken, got %v", err) } } func TestGetVideoOutputs(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() outputs, err := client.GetVideoOutputs(ctx) if err != nil { t.Fatalf("GetVideoOutputs failed: %v", err) } if len(outputs) != 1 { t.Fatalf("Expected 1 video output, got %d", len(outputs)) } if outputs[0].Token != "video_out_001" { t.Errorf("Expected video output token 'video_out_001', got '%s'", outputs[0].Token) } if outputs[0].Resolution == nil { t.Fatal("Expected Resolution to be set") } if outputs[0].Resolution.Width != 1920 { t.Errorf("Expected resolution width 1920, got %d", outputs[0].Resolution.Width) } if outputs[0].Resolution.Height != 1080 { t.Errorf("Expected resolution height 1080, got %d", outputs[0].Resolution.Height) } if outputs[0].RefreshRate != 60.0 { t.Errorf("Expected refresh rate 60.0, got %f", outputs[0].RefreshRate) } if outputs[0].AspectRatio != "16:9" { t.Errorf("Expected aspect ratio '16:9', got '%s'", outputs[0].AspectRatio) } } func TestGetSerialPorts(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() ports, err := client.GetSerialPorts(ctx) if err != nil { t.Fatalf("GetSerialPorts failed: %v", err) } if len(ports) != 2 { t.Fatalf("Expected 2 serial ports, got %d", len(ports)) } if ports[0].Token != "serial_001" { t.Errorf("Expected first serial port token 'serial_001', got '%s'", ports[0].Token) } if ports[0].Type != SerialPortTypeRS232 { t.Errorf("Expected first serial port type RS232, got '%s'", ports[0].Type) } if ports[1].Type != SerialPortTypeRS485 { t.Errorf("Expected second serial port type RS485, got '%s'", ports[1].Type) } } func TestGetSerialPortConfiguration(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() config, err := client.GetSerialPortConfiguration(ctx, "serial_001") if err != nil { t.Fatalf("GetSerialPortConfiguration failed: %v", err) } if config.Token != "serial_001" { t.Errorf("Expected token 'serial_001', got '%s'", config.Token) } if config.Type != SerialPortTypeRS232 { t.Errorf("Expected type RS232, got '%s'", config.Type) } if config.BaudRate != 9600 { t.Errorf("Expected baud rate 9600, got %d", config.BaudRate) } if config.ParityBit != ParityNone { t.Errorf("Expected parity None, got '%s'", config.ParityBit) } if config.CharacterLength != 8 { t.Errorf("Expected character length 8, got %d", config.CharacterLength) } if config.StopBit != 1 { t.Errorf("Expected stop bit 1, got %f", config.StopBit) } } func TestGetSerialPortConfigurationInvalidToken(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() _, err = client.GetSerialPortConfiguration(ctx, "") if !errors.Is(err, ErrInvalidSerialPortToken) { t.Errorf("Expected ErrInvalidSerialPortToken, got %v", err) } } func TestGetSerialPortConfigurationOptions(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() options, err := client.GetSerialPortConfigurationOptions(ctx, "serial_001") if err != nil { t.Fatalf("GetSerialPortConfigurationOptions failed: %v", err) } if len(options.BaudRateList) != 3 { t.Errorf("Expected 3 baud rate options, got %d", len(options.BaudRateList)) } if len(options.ParityBitList) != 3 { t.Errorf("Expected 3 parity bit options, got %d", len(options.ParityBitList)) } if len(options.CharacterLengthList) != 2 { t.Errorf("Expected 2 character length options, got %d", len(options.CharacterLengthList)) } if len(options.StopBitList) != 2 { t.Errorf("Expected 2 stop bit options, got %d", len(options.StopBitList)) } } func TestGetSerialPortConfigurationOptionsInvalidToken(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() _, err = client.GetSerialPortConfigurationOptions(ctx, "") if !errors.Is(err, ErrInvalidSerialPortToken) { t.Errorf("Expected ErrInvalidSerialPortToken, got %v", err) } } func TestSetSerialPortConfiguration(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() config := &SerialPortConfiguration{ Token: "serial_001", Type: SerialPortTypeRS232, BaudRate: 19200, ParityBit: ParityNone, CharacterLength: 8, StopBit: 1, } err = client.SetSerialPortConfiguration(ctx, config) if err != nil { t.Fatalf("SetSerialPortConfiguration failed: %v", err) } } func TestSetSerialPortConfigurationValidation(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() // Test nil config. err = client.SetSerialPortConfiguration(ctx, nil) if !errors.Is(err, ErrSerialPortConfigNil) { t.Errorf("Expected ErrSerialPortConfigNil, got %v", err) } // Test empty token. config := &SerialPortConfiguration{Token: ""} err = client.SetSerialPortConfiguration(ctx, config) if !errors.Is(err, ErrInvalidSerialPortToken) { t.Errorf("Expected ErrInvalidSerialPortToken, got %v", err) } } func TestSendReceiveSerialCommand(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() response, err := client.SendReceiveSerialCommand(ctx, "serial_001", []byte("HELLO"), 5, 10) if err != nil { t.Fatalf("SendReceiveSerialCommand failed: %v", err) } if string(response) != "OK" { t.Errorf("Expected response 'OK', got '%s'", string(response)) } } func TestSendReceiveSerialCommandValidation(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() // Test empty token. _, err = client.SendReceiveSerialCommand(ctx, "", []byte("HELLO"), 5, 10) if !errors.Is(err, ErrInvalidSerialPortToken) { t.Errorf("Expected ErrInvalidSerialPortToken, got %v", err) } // Test empty data. _, err = client.SendReceiveSerialCommand(ctx, "serial_001", []byte{}, 5, 10) if !errors.Is(err, ErrInvalidSerialData) { t.Errorf("Expected ErrInvalidSerialData, got %v", err) } } func TestDigitalIdleStateConstants(t *testing.T) { if DigitalIdleOpen != "open" { t.Errorf("DigitalIdleOpen should be 'open'") } if DigitalIdleClosed != "closed" { t.Errorf("DigitalIdleClosed should be 'closed'") } } func TestSerialPortTypeConstants(t *testing.T) { if SerialPortTypeRS232 != "RS232" { t.Errorf("SerialPortTypeRS232 should be 'RS232'") } if SerialPortTypeRS422 != "RS422" { t.Errorf("SerialPortTypeRS422 should be 'RS422'") } if SerialPortTypeRS485 != "RS485" { t.Errorf("SerialPortTypeRS485 should be 'RS485'") } if SerialPortTypeGeneric != "Generic" { t.Errorf("SerialPortTypeGeneric should be 'Generic'") } } func TestParityBitConstants(t *testing.T) { if ParityNone != "None" { t.Errorf("ParityNone should be 'None'") } if ParityOdd != "Odd" { t.Errorf("ParityOdd should be 'Odd'") } if ParityEven != "Even" { t.Errorf("ParityEven should be 'Even'") } if ParityMark != "Mark" { t.Errorf("ParityMark should be 'Mark'") } if ParitySpace != "Space" { t.Errorf("ParitySpace should be 'Space'") } } func TestGetVideoOutputConfiguration(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() config, err := client.GetVideoOutputConfiguration(ctx, "video_out_001") if err != nil { t.Fatalf("GetVideoOutputConfiguration failed: %v", err) } if config.Token != "config_001" { t.Errorf("Expected token 'config_001', got '%s'", config.Token) } if config.Name != "Main Output" { t.Errorf("Expected name 'Main Output', got '%s'", config.Name) } if config.UseCount != 2 { t.Errorf("Expected use count 2, got %d", config.UseCount) } if config.OutputToken != "video_out_001" { t.Errorf("Expected output token 'video_out_001', got '%s'", config.OutputToken) } } func TestGetVideoOutputConfigurationInvalidToken(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() _, err = client.GetVideoOutputConfiguration(ctx, "") if !errors.Is(err, ErrInvalidVideoOutputToken) { t.Errorf("Expected ErrInvalidVideoOutputToken, got %v", err) } } func TestGetVideoOutputConfigurationOptions(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() options, err := client.GetVideoOutputConfigurationOptions(ctx, "video_out_001") if err != nil { t.Fatalf("GetVideoOutputConfigurationOptions failed: %v", err) } if options.Name.Min != 1 { t.Errorf("Expected Name.Min to be 1, got %d", options.Name.Min) } if options.Name.Max != 64 { t.Errorf("Expected Name.Max to be 64, got %d", options.Name.Max) } if len(options.OutputTokensAvailable) != 2 { t.Errorf("Expected 2 output tokens available, got %d", len(options.OutputTokensAvailable)) } } func TestGetVideoOutputConfigurationOptionsInvalidToken(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() _, err = client.GetVideoOutputConfigurationOptions(ctx, "") if !errors.Is(err, ErrInvalidVideoOutputToken) { t.Errorf("Expected ErrInvalidVideoOutputToken, got %v", err) } } func TestSetVideoOutputConfiguration(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() config := &VideoOutputConfiguration{ Token: "config_001", Name: "Main Output", UseCount: 2, OutputToken: "video_out_001", ForcePersistence: true, } err = client.SetVideoOutputConfiguration(ctx, config) if err != nil { t.Fatalf("SetVideoOutputConfiguration failed: %v", err) } } func TestSetVideoOutputConfigurationValidation(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() // Test nil config. err = client.SetVideoOutputConfiguration(ctx, nil) if !errors.Is(err, ErrVideoOutputConfigNil) { t.Errorf("Expected ErrVideoOutputConfigNil, got %v", err) } // Test empty token. config := &VideoOutputConfiguration{Token: ""} err = client.SetVideoOutputConfiguration(ctx, config) if !errors.Is(err, ErrInvalidVideoOutputToken) { t.Errorf("Expected ErrInvalidVideoOutputToken, got %v", err) } } func TestGetRelayOutputOptions(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() options, err := client.GetRelayOutputOptions(ctx, "relay_001") if err != nil { t.Fatalf("GetRelayOutputOptions failed: %v", err) } if options.Token != "relay_001" { t.Errorf("Expected token 'relay_001', got '%s'", options.Token) } if len(options.Mode) != 2 { t.Errorf("Expected 2 modes, got %d", len(options.Mode)) } if options.Mode[0] != RelayModeMonostable { t.Errorf("Expected first mode to be Monostable, got '%s'", options.Mode[0]) } if options.Mode[1] != RelayModeBistable { t.Errorf("Expected second mode to be Bistable, got '%s'", options.Mode[1]) } if len(options.DelayTimes) != 3 { t.Errorf("Expected 3 delay times, got %d", len(options.DelayTimes)) } if !options.Discrete { t.Error("Expected Discrete to be true") } } func TestGetRelayOutputOptionsInvalidToken(t *testing.T) { server := newMockDeviceIOServer() defer server.Close() client, err := NewClient(server.URL, WithCredentials("admin", "password")) if err != nil { t.Fatalf("Failed to create client: %v", err) } ctx := context.Background() _, err = client.GetRelayOutputOptions(ctx, "") if !errors.Is(err, ErrInvalidRelayOutputToken) { t.Errorf("Expected ErrInvalidRelayOutputToken, got %v", err) } }