From c7ef445d6ad56e4b0225155ff703789bcc5700b9 Mon Sep 17 00:00:00 2001
From: Sebastian Norling <1932208+Bazze@users.noreply.github.com>
Date: Wed, 27 May 2026 18:07:56 +0200
Subject: [PATCH] refactor(onvif): add SendSoapWithOptions as the variadic
workhorse
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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.
---
Device.go | 31 +++++++++++++++++++++++++++++--
Device_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 72 insertions(+), 2 deletions(-)
diff --git a/Device.go b/Device.go
index a4d2a1c..c79d43b 100644
--- a/Device.go
+++ b/Device.go
@@ -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 != "" {
diff --git a/Device_test.go b/Device_test.go
index b6c2d5a..7c6d96e 100644
--- a/Device_test.go
+++ b/Device_test.go
@@ -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 = `42`
+ const bodyXML = ``
+
+ 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, ``)
+ 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