package onvif import ( "context" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" ) // Mock ONVIF server for comprehensive testing type MockONVIFServer struct { server *httptest.Server responses map[string]string username string password string authFailed bool } func NewMockONVIFServer() *MockONVIFServer { mock := &MockONVIFServer{ responses: make(map[string]string), username: "admin", password: "password", } mux := http.NewServeMux() mux.HandleFunc("/", mock.handleRequest) mock.server = httptest.NewServer(mux) // Set up default responses mock.setupDefaultResponses() return mock } func (m *MockONVIFServer) URL() string { return m.server.URL } func (m *MockONVIFServer) Close() { m.server.Close() } func (m *MockONVIFServer) SetAuthFailure(fail bool) { m.authFailed = fail } func (m *MockONVIFServer) SetResponse(action string, response string) { m.responses[action] = response } func (m *MockONVIFServer) handleRequest(w http.ResponseWriter, r *http.Request) { // Read request body body := make([]byte, 0) if r.Body != nil { defer r.Body.Close() buf := make([]byte, 1024) for { n, err := r.Body.Read(buf) if n > 0 { body = append(body, buf[:n]...) } if err != nil { break } } } requestBody := string(body) // Simple auth check if m.authFailed && strings.Contains(requestBody, "UsernameToken") { w.WriteHeader(http.StatusUnauthorized) return } // Determine action var action string if strings.Contains(requestBody, "GetDeviceInformation") { action = "GetDeviceInformation" } else if strings.Contains(requestBody, "GetCapabilities") { action = "GetCapabilities" } else if strings.Contains(requestBody, "GetProfiles") { action = "GetProfiles" } else if strings.Contains(requestBody, "GetStreamURI") { action = "GetStreamURI" } else if strings.Contains(requestBody, "GetStatus") { action = "GetStatus" } else { action = "default" } response, exists := m.responses[action] if !exists { response = m.responses["default"] } w.Header().Set("Content-Type", "application/soap+xml") w.WriteHeader(http.StatusOK) w.Write([]byte(response)) } func (m *MockONVIFServer) setupDefaultResponses() { // GetDeviceInformation response m.responses["GetDeviceInformation"] = ` Test Camera Inc TestCam 3000 1.0.0 12345 HW001 ` // GetCapabilities response m.responses["GetCapabilities"] = ` ` + m.server.URL + `/onvif/device_service ` + m.server.URL + `/onvif/media_service ` + m.server.URL + `/onvif/ptz_service ` // GetProfiles response m.responses["GetProfiles"] = ` Main Profile H264 1920 1080 ` // Default fault response m.responses["default"] = ` soap:Receiver Action not supported in mock ` } func TestNewClient(t *testing.T) { tests := []struct { name string endpoint string wantError bool }{ { name: "valid http endpoint", endpoint: "http://192.168.1.100/onvif/device_service", wantError: false, }, { name: "valid https endpoint", endpoint: "https://camera.example.com/onvif", wantError: false, }, { name: "invalid endpoint", endpoint: "not a url", wantError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { client, err := NewClient(tt.endpoint) if (err != nil) != tt.wantError { t.Errorf("NewClient() error = %v, wantError %v", err, tt.wantError) return } if !tt.wantError && client == nil { t.Error("NewClient() returned nil client") } }) } } func TestClientOptions(t *testing.T) { endpoint := "http://192.168.1.100/onvif" t.Run("WithCredentials", func(t *testing.T) { username := "admin" password := "test123" client, err := NewClient(endpoint, WithCredentials(username, password)) if err != nil { t.Fatalf("NewClient() error = %v", err) } gotUser, gotPass := client.GetCredentials() if gotUser != username || gotPass != password { t.Errorf("GetCredentials() = (%v, %v), want (%v, %v)", gotUser, gotPass, username, password) } }) t.Run("WithTimeout", func(t *testing.T) { timeout := 10 * time.Second client, err := NewClient(endpoint, WithTimeout(timeout)) if err != nil { t.Fatalf("NewClient() error = %v", err) } if client.httpClient.Timeout != timeout { t.Errorf("HTTP client timeout = %v, want %v", client.httpClient.Timeout, timeout) } }) t.Run("WithHTTPClient", func(t *testing.T) { customClient := &http.Client{ Timeout: 5 * time.Second, } client, err := NewClient(endpoint, WithHTTPClient(customClient)) if err != nil { t.Fatalf("NewClient() error = %v", err) } if client.httpClient != customClient { t.Error("Custom HTTP client not set") } }) } func TestClientEndpoint(t *testing.T) { endpoint := "http://192.168.1.100/onvif" client, err := NewClient(endpoint) if err != nil { t.Fatalf("NewClient() error = %v", err) } if got := client.Endpoint(); got != endpoint { t.Errorf("Endpoint() = %v, want %v", got, endpoint) } } func TestClientSetCredentials(t *testing.T) { client, err := NewClient("http://192.168.1.100/onvif") if err != nil { t.Fatalf("NewClient() error = %v", err) } username := "newuser" password := "newpass" client.SetCredentials(username, password) gotUser, gotPass := client.GetCredentials() if gotUser != username || gotPass != password { t.Errorf("After SetCredentials(), GetCredentials() = (%v, %v), want (%v, %v)", gotUser, gotPass, username, password) } } func TestGetDeviceInformationWithMockServer(t *testing.T) { // Simple test server that returns HTTP 200 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/soap+xml") w.WriteHeader(http.StatusOK) // Return empty response - will cause EOF error which is expected for now })) defer server.Close() client, err := NewClient( server.URL, WithCredentials("admin", "password"), ) if err != nil { t.Fatalf("NewClient() failed: %v", err) } ctx := context.Background() _, err = client.GetDeviceInformation(ctx) // We expect an error since we're not returning valid SOAP if err == nil { t.Errorf("Expected error with empty response, but got none") } // This test just verifies the client can be created and make requests t.Logf("Expected error occurred: %v", err) } func TestGetDeviceInformationWithAuth(t *testing.T) { // Test unauthorized response server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) })) defer server.Close() client, err := NewClient(server.URL) if err != nil { t.Fatalf("NewClient() failed: %v", err) } ctx := context.Background() _, err = client.GetDeviceInformation(ctx) if err == nil { t.Errorf("Expected authentication error, but got none") } t.Logf("Authentication error (expected): %v", err) } func TestInitializeEndpointDiscovery(t *testing.T) { // Test that Initialize can handle network errors gracefully client, err := NewClient( "http://192.168.999.999/onvif/device_service", // non-existent IP WithCredentials("admin", "password"), WithTimeout(1*time.Second), ) if err != nil { t.Fatalf("NewClient() failed: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() err = client.Initialize(ctx) // We expect this to fail due to network timeout if err == nil { t.Errorf("Expected network error, but got none") } t.Logf("Network error (expected): %v", err) } func TestGetProfilesRequiresInitialization(t *testing.T) { client, err := NewClient( "http://192.168.1.100/onvif/device_service", WithCredentials("admin", "password"), ) if err != nil { t.Fatalf("NewClient() failed: %v", err) } ctx := context.Background() _, err = client.GetProfiles(ctx) // Should fail because Initialize was not called if err == nil { t.Errorf("Expected error when GetProfiles called without Initialize") } t.Logf("Expected error: %v", err) } func TestContextTimeout(t *testing.T) { mock := NewMockONVIFServer() defer mock.Close() client, err := NewClient( mock.URL(), WithCredentials("admin", "password"), ) if err != nil { t.Fatalf("NewClient() failed: %v", err) } // Create context with very short timeout ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond) defer cancel() // This should timeout _, err = client.GetDeviceInformation(ctx) if err == nil { t.Errorf("Expected timeout error, but got none") } if !strings.Contains(err.Error(), "context deadline exceeded") { t.Errorf("Expected context deadline exceeded error, got: %v", err) } } func TestONVIFError(t *testing.T) { err := NewONVIFError("Sender", "InvalidArgs", "Invalid parameter value") if err.Code != "Sender" { t.Errorf("Code = %v, want %v", err.Code, "Sender") } if err.Reason != "InvalidArgs" { t.Errorf("Reason = %v, want %v", err.Reason, "InvalidArgs") } expectedError := "ONVIF error [Sender]: InvalidArgs - Invalid parameter value" if err.Error() != expectedError { t.Errorf("Error() = %v, want %v", err.Error(), expectedError) } if !IsONVIFError(err) { t.Error("IsONVIFError() returned false for ONVIF error") } } func BenchmarkNewClient(b *testing.B) { endpoint := "http://192.168.1.100/onvif" b.ResetTimer() for i := 0; i < b.N; i++ { _, err := NewClient(endpoint) if err != nil { b.Fatal(err) } } } func BenchmarkGetDeviceInformation(b *testing.B) { mock := NewMockONVIFServer() defer mock.Close() client, err := NewClient( mock.URL(), WithCredentials("admin", "password"), ) if err != nil { b.Fatalf("NewClient() failed: %v", err) } ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { _, err := client.GetDeviceInformation(ctx) if err != nil { b.Fatalf("GetDeviceInformation() failed: %v", err) } } } // Example test func ExampleClient_GetDeviceInformation() { // Create client client, err := NewClient( "http://192.168.1.100/onvif/device_service", WithCredentials("admin", "password"), WithTimeout(30*time.Second), ) if err != nil { panic(err) } // Get device information ctx := context.Background() info, err := client.GetDeviceInformation(ctx) if err != nil { panic(err) } fmt.Printf("Camera: %s %s\n", info.Manufacturer, info.Model) fmt.Printf("Firmware: %s\n", info.FirmwareVersion) }