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