Files
yovision/Sense/internal/onvif/http_test.go
T
QiuSW 9567838045
Harness governance / validate (push) Has been cancelled
Harness governance / validate (pull_request) Has been cancelled
feat(sense): complete T-006 five-stream integration
2026-08-07 16:33:35 +08:00

137 lines
5.7 KiB
Go

package onvif
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
type staticCredentials struct {
value Credentials
err error
}
func (s staticCredentials) Resolve(string) (Credentials, error) {
return s.value, s.err
}
func TestHTTPAdapterDiscoversMediaAndRewritesNATStream(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if !strings.Contains(request.Header.Get("Content-Type"), "action=") {
t.Fatal("SOAP action is required")
}
body := ""
switch {
case strings.Contains(request.Header.Get("Content-Type"), "GetDeviceInformation"):
body = `<tds:GetDeviceInformationResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl"><tds:Manufacturer>HIKVISION</tds:Manufacturer><tds:Model>camera</tds:Model><tds:FirmwareVersion>v1</tds:FirmwareVersion><tds:SerialNumber>serial</tds:SerialNumber></tds:GetDeviceInformationResponse>`
case strings.Contains(request.Header.Get("Content-Type"), "GetServices"):
body = `<tds:GetServicesResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl"><tds:Service><tds:Namespace>http://www.onvif.org/ver10/media/wsdl</tds:Namespace><tds:XAddr>http://192.0.2.10/onvif/Media</tds:XAddr></tds:Service></tds:GetServicesResponse>`
case strings.Contains(request.Header.Get("Content-Type"), "GetProfiles"):
body = `<trt:GetProfilesResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl"><trt:Profiles token="main"><trt:Name>Main</trt:Name><trt:VideoEncoderConfiguration/></trt:Profiles></trt:GetProfilesResponse>`
case strings.Contains(request.Header.Get("Content-Type"), "GetStreamUri"):
body = `<trt:GetStreamUriResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl"><trt:MediaUri><trt:Uri>rtsp://192.0.2.10:554/Streaming/Channels/101?transportmode=unicast&amp;profile=Profile_1</trt:Uri></trt:MediaUri></trt:GetStreamUriResponse>`
default:
http.Error(writer, "unexpected action", http.StatusBadRequest)
return
}
writer.Header().Set("Content-Type", "application/soap+xml")
_, _ = fmt.Fprintf(writer, `<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"><s:Body>%s</s:Body></s:Envelope>`, body)
}))
defer server.Close()
credentials := Credentials{
ONVIFUsername: "onvif-user", ONVIFPassword: "onvif-password",
RTSPUsername: "rtsp-user", RTSPPassword: "rtsp-password",
}
adapter := NewHTTPAdapter(staticCredentials{value: credentials}, server.Client(), HTTPOptions{
RTSPRewriteHost: "127.0.0.1", RTSPRewritePort: 10554, StripRTSPQuery: true,
})
result, err := adapter.Probe(context.Background(), Target{
EndpointRef: server.URL + "/onvif/device_service", CredentialRef: "env://camera",
})
if err != nil {
t.Fatal(err)
}
if result.Manufacturer != "HIKVISION" || result.Model != "camera" || len(result.Profiles) != 1 {
t.Fatalf("unexpected probe result: %+v", result)
}
stream, err := url.Parse(result.StreamURI)
if err != nil {
t.Fatal(err)
}
if stream.Host != "127.0.0.1:10554" || stream.Path != "/Streaming/Channels/101" || stream.RawQuery != "" {
t.Fatalf("unexpected rewritten stream address: host=%s path=%s", stream.Host, stream.Path)
}
password, _ := stream.User.Password()
if stream.User.Username() != credentials.RTSPUsername || password != credentials.RTSPPassword {
t.Fatal("RTSP credentials were not injected")
}
}
func TestHTTPAdapterPreservesRTSPQueryByDefault(t *testing.T) {
t.Parallel()
adapter := NewHTTPAdapter(staticCredentials{}, nil, HTTPOptions{})
streamURI, err := adapter.rewriteStreamURI(
&url.URL{Scheme: "http", Host: "camera.example:8008"},
"rtsp://192.0.2.10:554/live?profile=main",
Credentials{RTSPUsername: "user", RTSPPassword: "secret"},
)
if err != nil {
t.Fatal(err)
}
stream, err := url.Parse(streamURI)
if err != nil {
t.Fatal(err)
}
if stream.RawQuery != "profile=main" {
t.Fatalf("RTSP query was unexpectedly changed: %q", stream.RawQuery)
}
}
func TestHTTPAdapterMapsAuthorizationFaultWithoutLeakingSecret(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
writer.WriteHeader(http.StatusBadRequest)
_, _ = writer.Write([]byte(`<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"><s:Body><s:Fault><s:Reason><s:Text>The action requires authorization</s:Text></s:Reason></s:Fault></s:Body></s:Envelope>`))
}))
defer server.Close()
secret := "not-for-errors"
adapter := NewHTTPAdapter(staticCredentials{value: Credentials{
ONVIFUsername: "user", ONVIFPassword: secret, RTSPUsername: "user", RTSPPassword: secret,
}}, server.Client(), HTTPOptions{})
_, err := adapter.Probe(context.Background(), Target{EndpointRef: server.URL, CredentialRef: "env://camera"})
if CodeOf(err) != ErrorAuthentication || strings.Contains(err.Error(), secret) {
t.Fatalf("expected redacted authentication error, got %v", err)
}
}
func TestEnvCredentialsAndDirectRTSPRouting(t *testing.T) {
t.Parallel()
values := map[string]string{
"SENSE_CREDENTIAL_CAMERA_ONVIF_USERNAME": "onvif",
"SENSE_CREDENTIAL_CAMERA_ONVIF_PASSWORD": "onvif-secret",
"SENSE_CREDENTIAL_CAMERA_RTSP_USERNAME": "rtsp",
"SENSE_CREDENTIAL_CAMERA_RTSP_PASSWORD": "rtsp-secret",
}
provider := EnvCredentials{LookupEnv: func(name string) (string, bool) {
value, ok := values[name]
return value, ok
}}
resolved, err := provider.Resolve("env://camera")
if err != nil || resolved.RTSPUsername != "rtsp" {
t.Fatalf("resolve credentials: %+v err=%v", resolved, err)
}
router := NewRouter(UnavailableAdapter{}, provider)
result, err := router.Probe(context.Background(), Target{
EndpointRef: "rtsp://127.0.0.1:8555/synthetic-1",
})
if err != nil || result.StreamURI != "rtsp://127.0.0.1:8555/synthetic-1" {
t.Fatalf("route direct RTSP: %+v err=%v", result, err)
}
}