Config now accepts host/port/channel/username/password (untracked local config only) or rtsp_url_env; net/url percent-encodes the userinfo so a password with @/:/ builds a valid RTSP URL (fixes the manual %40 pain). Adds WriteLocalCameraSource / WriteLocalEventTuning for the settings form. Guards kept: still rejects a full url/rtsp_url; credentials stay out of the config version; a test asserts the public example has no credentials. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
141 lines
4.2 KiB
Go
141 lines
4.2 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func writeTempConfig(t *testing.T, content string) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "config.json")
|
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func structuredConfigJSON() string {
|
|
return strings.Replace(validConfigJSON(),
|
|
`"rtsp_url_env": "SILVER_POSE_RTSP_URL",`,
|
|
`"host": "192.0.2.10", "port": 554, "channel": "102", "username": "admin", "password": "p@ss/w:d",`,
|
|
1)
|
|
}
|
|
|
|
func TestStructuredSourceBuildsLosslessEncodedURL(t *testing.T) {
|
|
cfg, err := Load(writeTempConfig(t, structuredConfigJSON()))
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if strings.Contains(cfg.Source.URL, "p@ss") {
|
|
t.Fatalf("password @ was not percent-encoded: %q", cfg.Source.URL)
|
|
}
|
|
parsed, err := url.Parse(cfg.Source.URL)
|
|
if err != nil {
|
|
t.Fatalf("built URL does not parse: %v", err)
|
|
}
|
|
if parsed.Hostname() != "192.0.2.10" || parsed.Port() != "554" {
|
|
t.Fatalf("host/port = %q/%q", parsed.Hostname(), parsed.Port())
|
|
}
|
|
if parsed.User.Username() != "admin" {
|
|
t.Fatalf("username = %q", parsed.User.Username())
|
|
}
|
|
if password, _ := parsed.User.Password(); password != "p@ss/w:d" {
|
|
t.Fatalf("password did not round-trip: %q", password)
|
|
}
|
|
if parsed.Path != "/Streaming/Channels/102" {
|
|
t.Fatalf("path = %q", parsed.Path)
|
|
}
|
|
}
|
|
|
|
func TestStructuredSourceRequiresPassword(t *testing.T) {
|
|
noPassword := strings.Replace(structuredConfigJSON(), `"password": "p@ss/w:d",`, "", 1)
|
|
if _, err := Load(writeTempConfig(t, noPassword)); err == nil || !strings.Contains(err.Error(), "password") {
|
|
t.Fatalf("expected password error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStructuredCredentialsExcludedFromConfigVersion(t *testing.T) {
|
|
first, err := Load(writeTempConfig(t, structuredConfigJSON()))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
other := strings.Replace(structuredConfigJSON(), `"password": "p@ss/w:d"`, `"password": "different"`, 1)
|
|
second, err := Load(writeTempConfig(t, other))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.Source.URL == second.Source.URL {
|
|
t.Fatal("test setup did not change the credential")
|
|
}
|
|
if first.RuntimeConfigVersion != second.RuntimeConfigVersion {
|
|
t.Fatal("runtime config version changed with a credential")
|
|
}
|
|
}
|
|
|
|
func TestPublicExampleHasNoEmbeddedCredentials(t *testing.T) {
|
|
data, err := os.ReadFile(filepath.Join("..", "..", "config.example.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var document struct {
|
|
Source map[string]json.RawMessage `json:"source"`
|
|
}
|
|
if err := json.Unmarshal(data, &document); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, forbidden := range []string{"host", "username", "password", "url", "rtsp_url"} {
|
|
if _, present := document.Source[forbidden]; present {
|
|
t.Fatalf("public example leaked credential field %q", forbidden)
|
|
}
|
|
}
|
|
if _, present := document.Source["rtsp_url_env"]; !present {
|
|
t.Fatal("public example missing rtsp_url_env")
|
|
}
|
|
}
|
|
|
|
func TestWriteLocalCameraSourceRoundTrips(t *testing.T) {
|
|
path := writeTempConfig(t, validConfigJSON())
|
|
if err := WriteLocalCameraSource(path, "hik", "192.0.2.20", 554, "102", "operator", "s@cret", "tcp", 5, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("reload: %v", err)
|
|
}
|
|
if cfg.Source.ID != "hik" {
|
|
t.Fatalf("id = %q", cfg.Source.ID)
|
|
}
|
|
parsed, _ := url.Parse(cfg.Source.URL)
|
|
if parsed.Hostname() != "192.0.2.20" {
|
|
t.Fatalf("host = %q", parsed.Hostname())
|
|
}
|
|
if password, _ := parsed.User.Password(); password != "s@cret" {
|
|
t.Fatalf("password = %q", password)
|
|
}
|
|
}
|
|
|
|
func TestWriteLocalEventTuningRoundTrips(t *testing.T) {
|
|
path := writeTempConfig(t, validConfigJSON())
|
|
t.Setenv("SILVER_POSE_RTSP_URL", "rtsp://demo.invalid/live")
|
|
if err := WriteLocalEventTuning(path, 0.5, 2.5, 35, 0.3, true, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("reload: %v", err)
|
|
}
|
|
if cfg.Event.ConfirmWindowSeconds != 2.5 || cfg.Event.HorizontalAngleThresholdDegrees != 35 {
|
|
t.Fatalf("event tuning not applied: %+v", cfg.Event)
|
|
}
|
|
if !cfg.Event.RequireRapidDrop || !cfg.Event.RequireLowerBody {
|
|
t.Fatalf("boolean tuning not applied: %+v", cfg.Event)
|
|
}
|
|
if cfg.Model.ConfidenceThreshold != 0.3 {
|
|
t.Fatalf("model confidence = %v", cfg.Model.ConfidenceThreshold)
|
|
}
|
|
}
|