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