mirror of
https://github.com/kerberos-io/onvif.git
synced 2026-08-23 15:08:33 +00:00
Future per-call knobs (timeout, context, custom namespaces) will arrive sooner than later. Adding them as SendSoapWithHeader2 / 3 / N would turn the Device API into a combinatorial mess; adding them as new required positional args breaks every existing consumer. SendSoapWithOptions accepts variadic SoapOption values. SendSoap and SendSoapWithHeader become one-line delegates so all current callers keep their signatures, and the public surface is purely additive. Only WithHeader ships today — wiring the AXIS ReferenceParameters path through the same plumbing. New options land as WithX constructors in this file rather than as new Device methods.
198 lines
7.1 KiB
Go
198 lines
7.1 KiB
Go
package onvif
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestDevice_SetDeviceInfoFromScopes(t *testing.T) {
|
|
const (
|
|
name = "DeviceName"
|
|
hardware = "M9000"
|
|
)
|
|
scopes := []string{
|
|
"onvif://www.onvif.org/Profile/Streaming",
|
|
"onvif://www.onvif.org/SomethingElse/value",
|
|
"onvif://www.onvif.org/name/" + name,
|
|
"onvif://www.onvif.org/hardware/" + hardware,
|
|
}
|
|
device := Device{}
|
|
device.SetDeviceInfoFromScopes(scopes)
|
|
assert.Equal(t, device.info.Name, name)
|
|
assert.Equal(t, device.info.Model, hardware)
|
|
}
|
|
|
|
// TestDevice_SendSoapWithHeader_InjectsHeaderXML verifies that the
|
|
// supplied header XML lands inside the SOAP <Header> element of the
|
|
// outgoing request. AXIS-style WS-Addressing reference parameter
|
|
// echoing depends on this — without it the camera returns
|
|
// ter:InvalidArgs on every PullMessages.
|
|
func TestDevice_SendSoapWithHeader_InjectsHeaderXML(t *testing.T) {
|
|
const headerXML = `<dom0:SubscriptionId xmlns:dom0="http://www.axis.com/2009/event" wsa:IsReferenceParameter="true">297</dom0:SubscriptionId>`
|
|
const bodyXML = `<tev:PullMessages xmlns:tev="http://www.onvif.org/ver10/events/wsdl"><tev:Timeout>PT5S</tev:Timeout><tev:MessageLimit>32</tev:MessageLimit></tev:PullMessages>`
|
|
|
|
var captured string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
b, _ := io.ReadAll(r.Body)
|
|
captured = string(b)
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
dev := Device{
|
|
params: DeviceParams{
|
|
Xaddr: strings.TrimPrefix(srv.URL, "http://"),
|
|
HttpClient: srv.Client(),
|
|
},
|
|
}
|
|
resp, err := dev.SendSoapWithHeader(srv.URL, bodyXML, headerXML)
|
|
require.NoError(t, err)
|
|
if resp != nil && resp.Body != nil {
|
|
resp.Body.Close()
|
|
}
|
|
|
|
headerStart := strings.Index(captured, "Header>")
|
|
bodyStart := strings.Index(captured, "Body>")
|
|
require.NotEqual(t, -1, headerStart, "envelope must contain <Header>; got: %s", captured)
|
|
require.Greater(t, bodyStart, headerStart, "Body must follow Header in the envelope")
|
|
|
|
headerSlice := captured[headerStart:bodyStart]
|
|
assert.Contains(t, headerSlice, "SubscriptionId",
|
|
"injected header element must land inside SOAP <Header>")
|
|
assert.Contains(t, headerSlice, "297")
|
|
|
|
bodySlice := captured[bodyStart:]
|
|
assert.Contains(t, bodySlice, "PullMessages",
|
|
"body content must land inside SOAP <Body>")
|
|
}
|
|
|
|
// Per WS-Addressing 1.0 §3.1 every reference parameter is a separate
|
|
// SOAP Header block. Vendors that declare two ref params would silently
|
|
// produce a header-less request if the implementation only accepts a
|
|
// single top-level element.
|
|
func TestDevice_SendSoapWithHeader_AcceptsMultipleTopLevelChildren(t *testing.T) {
|
|
const headerXML = `<a:Foo xmlns:a="ns/a">1</a:Foo><b:Bar xmlns:b="ns/b">2</b:Bar>`
|
|
const bodyXML = `<tev:PullMessages xmlns:tev="http://www.onvif.org/ver10/events/wsdl"/>`
|
|
|
|
var captured string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
b, _ := io.ReadAll(r.Body)
|
|
captured = string(b)
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
dev := Device{params: DeviceParams{
|
|
Xaddr: strings.TrimPrefix(srv.URL, "http://"),
|
|
HttpClient: srv.Client(),
|
|
}}
|
|
resp, err := dev.SendSoapWithHeader(srv.URL, bodyXML, headerXML)
|
|
require.NoError(t, err)
|
|
if resp != nil && resp.Body != nil {
|
|
resp.Body.Close()
|
|
}
|
|
|
|
headerSlice := captured[strings.Index(captured, "Header>"):strings.Index(captured, "Body>")]
|
|
assert.Contains(t, headerSlice, "Foo")
|
|
assert.Contains(t, headerSlice, "Bar")
|
|
}
|
|
|
|
// SendSoapWithOptions is the variadic shape that future per-call
|
|
// options (timeout, context, ...) will hang off. SendSoap and
|
|
// SendSoapWithHeader stay as thin convenience wrappers so existing
|
|
// callers are not forced to migrate.
|
|
func TestDevice_SendSoapWithOptions_WithHeaderMatchesSendSoapWithHeader(t *testing.T) {
|
|
const headerXML = `<dom0:SubscriptionId xmlns:dom0="urn:test">42</dom0:SubscriptionId>`
|
|
const bodyXML = `<tev:PullMessages xmlns:tev="http://www.onvif.org/ver10/events/wsdl"/>`
|
|
|
|
var captured string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
b, _ := io.ReadAll(r.Body)
|
|
captured = string(b)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
dev := Device{params: DeviceParams{HttpClient: srv.Client()}}
|
|
resp, err := dev.SendSoapWithOptions(srv.URL, bodyXML, WithHeader(headerXML))
|
|
require.NoError(t, err)
|
|
if resp != nil && resp.Body != nil {
|
|
resp.Body.Close()
|
|
}
|
|
assert.Contains(t, captured, "SubscriptionId")
|
|
assert.Contains(t, captured, "42")
|
|
}
|
|
|
|
func TestDevice_SendSoapWithOptions_NoOptsMatchesSendSoap(t *testing.T) {
|
|
var captured string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
b, _ := io.ReadAll(r.Body)
|
|
captured = string(b)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
dev := Device{params: DeviceParams{HttpClient: srv.Client()}}
|
|
resp, err := dev.SendSoapWithOptions(srv.URL, `<tev:Body xmlns:tev="x"/>`)
|
|
require.NoError(t, err)
|
|
if resp != nil && resp.Body != nil {
|
|
resp.Body.Close()
|
|
}
|
|
assert.NotContains(t, captured, "IsReferenceParameter",
|
|
"no opts should produce a header-less envelope")
|
|
}
|
|
|
|
// Digest auth fallback path: the camera 401s the first POST and the
|
|
// retry computes a digest. The ref-params header must survive the
|
|
// retry — losing it would silently re-introduce the AXIS regression
|
|
// on every authenticated camera.
|
|
func TestDevice_SendSoapWithHeader_PreservesHeaderAcrossDigestRetry(t *testing.T) {
|
|
const headerXML = `<dom0:SubscriptionId xmlns:dom0="urn:vendor:axis" wsa:IsReferenceParameter="true">297</dom0:SubscriptionId>`
|
|
var capturedSecondBody string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Authorization") == "" {
|
|
w.Header().Set("WWW-Authenticate", `Digest realm="onvif", nonce="abc", qop="auth"`)
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
b, _ := io.ReadAll(r.Body)
|
|
capturedSecondBody = string(b)
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
dev := Device{params: DeviceParams{
|
|
Xaddr: strings.TrimPrefix(srv.URL, "http://"),
|
|
HttpClient: srv.Client(),
|
|
Username: "admin",
|
|
Password: "secret",
|
|
}}
|
|
resp, err := dev.SendSoapWithHeader(srv.URL, `<tev:PullMessages xmlns:tev="http://www.onvif.org/ver10/events/wsdl"/>`, headerXML)
|
|
require.NoError(t, err)
|
|
if resp != nil && resp.Body != nil {
|
|
resp.Body.Close()
|
|
}
|
|
assert.Contains(t, capturedSecondBody, "SubscriptionId",
|
|
"digest retry must carry the same ref-params header as the first attempt")
|
|
assert.Contains(t, capturedSecondBody, "297")
|
|
}
|
|
|
|
func TestDevice_SendSoapWithHeader_PropagatesMalformedHeaderError(t *testing.T) {
|
|
var hits int
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
hits++
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
dev := Device{params: DeviceParams{HttpClient: srv.Client()}}
|
|
|
|
_, err := dev.SendSoapWithHeader(srv.URL, "<body/>", "<not-closed")
|
|
require.Error(t, err)
|
|
assert.Equal(t, 0, hits,
|
|
"malformed header XML must fail fast — no request should reach the camera with a missing header block")
|
|
}
|