refactor(onvif): add SendSoapWithOptions as the variadic workhorse

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.
This commit is contained in:
Sebastian Norling
2026-05-27 18:07:56 +02:00
parent d726ed8edb
commit c7ef445d6a
2 changed files with 72 additions and 2 deletions

View File

@@ -335,7 +335,7 @@ func (dev *Device) GetEndpointByRequestStruct(requestStruct interface{}) (string
// SendSoap POSTs the given body wrapped in a SOAP envelope.
func (dev Device) SendSoap(endpoint string, xmlRequestBody string) (*http.Response, error) {
return dev.SendSoapWithHeader(endpoint, xmlRequestBody, "")
return dev.SendSoapWithOptions(endpoint, xmlRequestBody)
}
// SendSoapWithHeader is SendSoap plus arbitrary inner-Header XML —
@@ -350,11 +350,38 @@ func (dev Device) SendSoap(endpoint string, xmlRequestBody string) (*http.Respon
// sourced data inside it. Malformed XML returns an error before any
// request is made.
func (dev Device) SendSoapWithHeader(endpoint, xmlRequestBody, xmlHeaderContent string) (*http.Response, error) {
return dev.SendSoapWithOptions(endpoint, xmlRequestBody, WithHeader(xmlHeaderContent))
}
// SoapOption tweaks a single SendSoapWithOptions call. New options
// (per-call timeout, context, custom envelope namespaces, ...) should
// be added as WithX constructors here rather than as new method
// variants on Device.
type SoapOption func(*soapConfig)
type soapConfig struct {
headerContent string
}
// WithHeader adds inner-Header XML to the envelope. See
// SendSoapWithHeader for the content contract.
func WithHeader(xml string) SoapOption {
return func(c *soapConfig) { c.headerContent = xml }
}
// SendSoapWithOptions is the workhorse behind SendSoap and
// SendSoapWithHeader; call it directly when you need to combine
// options or pass options not surfaced by the convenience wrappers.
func (dev Device) SendSoapWithOptions(endpoint, xmlRequestBody string, opts ...SoapOption) (*http.Response, error) {
var cfg soapConfig
for _, o := range opts {
o(&cfg)
}
soap := gosoap.NewEmptySOAP()
soap.AddStringBodyContent(xmlRequestBody)
soap.AddRootNamespaces(Xlmns)
soap.AddAction()
if err := addHeaderChildren(&soap, xmlHeaderContent); err != nil {
if err := addHeaderChildren(&soap, cfg.headerContent); err != nil {
return nil, err
}
if dev.params.Username != "" && dev.params.Password != "" {

View File

@@ -103,6 +103,49 @@ func TestDevice_SendSoapWithHeader_AcceptsMultipleTopLevelChildren(t *testing.T)
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