96ac509c24
- Replaced http.NewRequest with http.NewRequestWithContext in client tests for better context management. - Updated method names and comments for clarity, including renaming GetWsdlUrl to GetWsdlURL and StorageUri to StorageURI for consistency. - Enhanced comments across various files to provide clearer descriptions of functionality and ONVIF specifications.
222 lines
5.5 KiB
Go
222 lines
5.5 KiB
Go
// Package onviftesting provides testing utilities for ONVIF client testing.
|
|
package onviftesting
|
|
|
|
import (
|
|
"archive/tar"
|
|
"compress/gzip"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// CapturedExchange represents a single SOAP request/response pair.
|
|
type CapturedExchange struct {
|
|
Timestamp string `json:"timestamp"`
|
|
Operation int `json:"operation"`
|
|
OperationName string `json:"operation_name,omitempty"`
|
|
Endpoint string `json:"endpoint"`
|
|
RequestBody string `json:"request_body"`
|
|
ResponseBody string `json:"response_body"`
|
|
StatusCode int `json:"status_code"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// CameraCapture holds all captured exchanges for a camera.
|
|
type CameraCapture struct {
|
|
CameraName string
|
|
Exchanges []CapturedExchange
|
|
}
|
|
|
|
// LoadCaptureFromArchive loads all captured exchanges from a tar.gz archive.
|
|
func LoadCaptureFromArchive(archivePath string) (*CameraCapture, error) {
|
|
file, err := os.Open(archivePath) //nolint:gosec // File path is from test data, safe
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open archive: %w", err)
|
|
}
|
|
defer func() {
|
|
//nolint:errcheck // Close error is not critical for cleanup
|
|
_ = file.Close()
|
|
}()
|
|
|
|
gzr, err := gzip.NewReader(file)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create gzip reader: %w", err)
|
|
}
|
|
defer func() {
|
|
//nolint:errcheck // Close error is not critical for cleanup
|
|
_ = gzr.Close()
|
|
}()
|
|
|
|
tr := tar.NewReader(gzr)
|
|
|
|
capture := &CameraCapture{
|
|
CameraName: filepath.Base(archivePath),
|
|
Exchanges: make([]CapturedExchange, 0),
|
|
}
|
|
|
|
// Read all .json files from the archive
|
|
for {
|
|
header, err := tr.Next()
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read tar header: %w", err)
|
|
}
|
|
|
|
// Only process JSON metadata files
|
|
if !strings.HasSuffix(header.Name, ".json") {
|
|
continue
|
|
}
|
|
|
|
data, err := io.ReadAll(tr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read file %s: %w", header.Name, err)
|
|
}
|
|
|
|
var exchange CapturedExchange
|
|
if err := json.Unmarshal(data, &exchange); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal %s: %w", header.Name, err)
|
|
}
|
|
|
|
capture.Exchanges = append(capture.Exchanges, exchange)
|
|
}
|
|
|
|
return capture, nil
|
|
}
|
|
|
|
// MockSOAPServer creates a test HTTP server that replays captured SOAP responses.
|
|
type MockSOAPServer struct {
|
|
Server *httptest.Server
|
|
Capture *CameraCapture
|
|
}
|
|
|
|
// NewMockSOAPServer creates a new mock server from a capture archive.
|
|
func NewMockSOAPServer(archivePath string) (*MockSOAPServer, error) {
|
|
capture, err := LoadCaptureFromArchive(archivePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mock := &MockSOAPServer{
|
|
Capture: capture,
|
|
}
|
|
|
|
// Create HTTP test server
|
|
mock.Server = httptest.NewServer(http.HandlerFunc(mock.handleRequest))
|
|
|
|
return mock, nil
|
|
}
|
|
|
|
// handleRequest matches incoming requests to captured responses.
|
|
func (m *MockSOAPServer) handleRequest(w http.ResponseWriter, r *http.Request) {
|
|
// Read request body
|
|
reqBody, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(w, "Failed to read request", http.StatusBadRequest)
|
|
|
|
return
|
|
}
|
|
|
|
// Extract operation name from request
|
|
operationName := extractOperationFromSOAP(string(reqBody))
|
|
|
|
// Find matching response by operation name
|
|
var exchange *CapturedExchange
|
|
|
|
if operationName != "" {
|
|
// Try matching by operation_name field if available
|
|
for i := range m.Capture.Exchanges {
|
|
if m.Capture.Exchanges[i].OperationName == operationName {
|
|
exchange = &m.Capture.Exchanges[i]
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
// If not found by operation_name, try matching by extracting from request body
|
|
if exchange == nil {
|
|
for i := range m.Capture.Exchanges {
|
|
capturedOp := extractOperationFromSOAP(m.Capture.Exchanges[i].RequestBody)
|
|
if capturedOp == operationName {
|
|
exchange = &m.Capture.Exchanges[i]
|
|
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if exchange == nil {
|
|
http.Error(w, fmt.Sprintf("No matching capture found for operation: %s", operationName), http.StatusNotFound)
|
|
|
|
return
|
|
}
|
|
|
|
// Return the captured response
|
|
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
|
|
w.WriteHeader(exchange.StatusCode)
|
|
//nolint:errcheck // Write error is not critical after WriteHeader
|
|
_, _ = w.Write([]byte(exchange.ResponseBody))
|
|
}
|
|
|
|
// Close shuts down the mock server.
|
|
func (m *MockSOAPServer) Close() {
|
|
m.Server.Close()
|
|
}
|
|
|
|
// URL returns the mock server's URL.
|
|
func (m *MockSOAPServer) URL() string {
|
|
return m.Server.URL
|
|
}
|
|
|
|
// extractOperationFromSOAP extracts the SOAP operation name from request body.
|
|
func extractOperationFromSOAP(soapBody string) string {
|
|
// Find the Body element
|
|
bodyStart := strings.Index(soapBody, "<Body")
|
|
if bodyStart == -1 {
|
|
return ""
|
|
}
|
|
|
|
// Find the closing > of the Body opening tag
|
|
bodyOpenEnd := strings.Index(soapBody[bodyStart:], ">")
|
|
if bodyOpenEnd == -1 {
|
|
return ""
|
|
}
|
|
bodyContentStart := bodyStart + bodyOpenEnd + 1
|
|
|
|
// Skip whitespace
|
|
for bodyContentStart < len(soapBody) && soapBody[bodyContentStart] <= ' ' {
|
|
bodyContentStart++
|
|
}
|
|
|
|
if bodyContentStart >= len(soapBody) || soapBody[bodyContentStart] != '<' {
|
|
return ""
|
|
}
|
|
|
|
// Extract the tag name
|
|
tagStart := bodyContentStart + 1
|
|
tagEnd := tagStart
|
|
for tagEnd < len(soapBody) && soapBody[tagEnd] != ' ' && soapBody[tagEnd] != '>' && soapBody[tagEnd] != '/' {
|
|
tagEnd++
|
|
}
|
|
|
|
if tagEnd > tagStart {
|
|
tagName := soapBody[tagStart:tagEnd]
|
|
// Remove namespace prefix if present
|
|
if colonIdx := strings.Index(tagName, ":"); colonIdx != -1 {
|
|
return tagName[colonIdx+1:]
|
|
}
|
|
|
|
return tagName
|
|
}
|
|
|
|
return ""
|
|
}
|