233 lines
8.2 KiB
Go
233 lines
8.2 KiB
Go
package config
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestValidateRejectsNonLoopbackByDefault(t *testing.T) {
|
|
t.Parallel()
|
|
cfg := Config{
|
|
HTTPAddress: "0.0.0.0:8080",
|
|
DatabaseDSN: "file:test.db",
|
|
MediaMTXURL: "http://127.0.0.1:9997",
|
|
ReconcileInterval: 1,
|
|
ProbeInterval: 1,
|
|
}
|
|
if err := cfg.Validate(); err == nil {
|
|
t.Fatal("expected non-loopback bind to be rejected")
|
|
}
|
|
cfg.AllowNonLoopback = true
|
|
if err := cfg.Validate(); err != nil {
|
|
t.Fatalf("explicit non-loopback opt-in failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateReconcileLeaseAndOrphanProductionBoundary(t *testing.T) {
|
|
base := Config{
|
|
HTTPAddress: "127.0.0.1:8080", DatabaseDriver: "postgres",
|
|
DatabaseDSN: "postgres://sense-runtime@127.0.0.1/yovision?sslmode=disable",
|
|
MediaMTXURL: "http://127.0.0.1:9997", ReconcileInterval: time.Second,
|
|
ProbeInterval: time.Second, ReconcileLeaseDuration: 30 * time.Second,
|
|
ReconcileOperationTimeout: 20 * time.Second, OrphanScanEnabled: true,
|
|
OrphanScanInterval: time.Minute, InstanceID: "ins_edge-01",
|
|
}
|
|
if err := base.Validate(); err != nil {
|
|
t.Fatalf("valid multi-instance configuration failed: %v", err)
|
|
}
|
|
invalidTimeout := base
|
|
invalidTimeout.ReconcileOperationTimeout = invalidTimeout.ReconcileLeaseDuration
|
|
if err := invalidTimeout.Validate(); err == nil {
|
|
t.Fatal("operation timeout equal to the lease was accepted")
|
|
}
|
|
invalidInstance := base
|
|
invalidInstance.InstanceID = "tenant/site"
|
|
if err := invalidInstance.Validate(); err == nil {
|
|
t.Fatal("unbounded instance label was accepted")
|
|
}
|
|
sqlite := base
|
|
sqlite.DatabaseDriver, sqlite.DatabaseDSN = "sqlite", "file:test.db"
|
|
if err := sqlite.Validate(); err == nil {
|
|
t.Fatal("orphan scanner was accepted on SQLite")
|
|
}
|
|
}
|
|
|
|
func TestValidateRejectsCredentialsInMediaMTXURL(t *testing.T) {
|
|
t.Parallel()
|
|
cfg := Config{
|
|
HTTPAddress: "127.0.0.1:8080",
|
|
DatabaseDSN: "file:test.db",
|
|
MediaMTXURL: "http://" + "user" + ":" + "redacted" + "@127.0.0.1:9997",
|
|
ReconcileInterval: 1,
|
|
ProbeInterval: 1,
|
|
}
|
|
if err := cfg.Validate(); err == nil {
|
|
t.Fatal("expected credentials in MediaMTX URL to be rejected")
|
|
}
|
|
}
|
|
|
|
func TestValidateONVIFModeAndRewritePort(t *testing.T) {
|
|
t.Parallel()
|
|
cfg := Config{
|
|
HTTPAddress: "127.0.0.1:8080",
|
|
DatabaseDSN: "file:test.db",
|
|
MediaMTXURL: "http://127.0.0.1:9997",
|
|
ReconcileInterval: 1,
|
|
ProbeInterval: 1,
|
|
ONVIFMode: "standard",
|
|
RTSPRewriteHost: "127.0.0.1",
|
|
RTSPRewritePort: 10554,
|
|
}
|
|
if err := cfg.Validate(); err != nil {
|
|
t.Fatalf("valid ONVIF configuration failed: %v", err)
|
|
}
|
|
cfg.ONVIFMode = "vendor"
|
|
if err := cfg.Validate(); err == nil {
|
|
t.Fatal("unknown ONVIF mode must be rejected")
|
|
}
|
|
cfg.ONVIFMode = "standard"
|
|
cfg.RTSPRewritePort = 65536
|
|
if err := cfg.Validate(); err == nil {
|
|
t.Fatal("invalid RTSP rewrite port must be rejected")
|
|
}
|
|
}
|
|
|
|
func TestLoadRTSPStripQueryOptIn(t *testing.T) {
|
|
t.Setenv("SENSE_ONVIF_RTSP_STRIP_QUERY", "true")
|
|
cfg, err := Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !cfg.RTSPStripQuery {
|
|
t.Fatal("explicit RTSP query stripping was not loaded")
|
|
}
|
|
}
|
|
|
|
func TestValidateDatabaseDriver(t *testing.T) {
|
|
t.Parallel()
|
|
cfg := Config{
|
|
HTTPAddress: "127.0.0.1:8080",
|
|
DatabaseDriver: "postgres",
|
|
DatabaseDSN: "file:test.db",
|
|
MediaMTXURL: "http://127.0.0.1:9997",
|
|
ReconcileInterval: 1,
|
|
ProbeInterval: 1,
|
|
}
|
|
if err := cfg.Validate(); err == nil {
|
|
t.Fatal("postgres driver must reject the SQLite default DSN")
|
|
}
|
|
cfg.DatabaseDSN = "postgres://sense-runtime@127.0.0.1/yovision?sslmode=disable"
|
|
if err := cfg.Validate(); err != nil {
|
|
t.Fatalf("valid PostgreSQL selection failed: %v", err)
|
|
}
|
|
cfg.DatabaseDriver = "mysql"
|
|
if err := cfg.Validate(); err == nil {
|
|
t.Fatal("unknown database driver must be rejected")
|
|
}
|
|
}
|
|
|
|
func TestValidateControlAPIRequiresPostgresAndExternalSecurityFiles(t *testing.T) {
|
|
cfg := Config{
|
|
HTTPAddress: "127.0.0.1:8080", DatabaseDriver: "sqlite", DatabaseDSN: "file:test.db",
|
|
MediaMTXURL: "http://127.0.0.1:9997", ReconcileInterval: 1, ProbeInterval: 1,
|
|
ControlAPIEnabled: true, ControlAuthMode: "static-sha256",
|
|
ControlAuthFile: filepath.Join(t.TempDir(), "sense-auth.json"),
|
|
ControlCursorKeyFile: filepath.Join(t.TempDir(), "sense-cursor.key"),
|
|
}
|
|
if err := cfg.Validate(); err == nil {
|
|
t.Fatal("Control API was accepted on SQLite")
|
|
}
|
|
cfg.DatabaseDriver = "postgres"
|
|
cfg.DatabaseDSN = "postgres://sense-runtime@127.0.0.1/yovision?sslmode=disable"
|
|
if err := cfg.Validate(); err != nil {
|
|
t.Fatalf("valid Control API configuration failed: %v", err)
|
|
}
|
|
cfg.ControlAuthFile = "relative-auth.json"
|
|
if err := cfg.Validate(); err == nil {
|
|
t.Fatal("repository-relative authentication file was accepted")
|
|
}
|
|
}
|
|
|
|
func TestValidateControlAPINonLoopbackNeedsSeparateRiskAcceptance(t *testing.T) {
|
|
cfg := Config{
|
|
HTTPAddress: "0.0.0.0:8080", AllowNonLoopback: true,
|
|
DatabaseDriver: "postgres", DatabaseDSN: "postgres://sense-runtime@127.0.0.1/yovision?sslmode=disable",
|
|
MediaMTXURL: "http://127.0.0.1:9997", ReconcileInterval: 1, ProbeInterval: 1,
|
|
ControlAPIEnabled: true, ControlAuthMode: "static-sha256",
|
|
ControlAuthFile: filepath.Join(t.TempDir(), "sense-auth.json"),
|
|
ControlCursorKeyFile: filepath.Join(t.TempDir(), "sense-cursor.key"),
|
|
}
|
|
if err := cfg.Validate(); err == nil {
|
|
t.Fatal("non-loopback plaintext Control API was accepted without explicit risk acceptance")
|
|
}
|
|
cfg.ControlAllowInsecureHTTP = true
|
|
if err := cfg.Validate(); err != nil {
|
|
t.Fatalf("explicit non-loopback Control API risk acceptance failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateSenseConsoleSecurityBoundary(t *testing.T) {
|
|
base := Config{
|
|
HTTPAddress: "127.0.0.1:8080", DatabaseDriver: "postgres",
|
|
DatabaseDSN: "postgres://sense-runtime@127.0.0.1/yovision?sslmode=disable",
|
|
MediaMTXURL: "http://127.0.0.1:9997", ReconcileInterval: time.Second, ProbeInterval: time.Second,
|
|
ControlAPIEnabled: true, ControlAuthMode: "static-sha256",
|
|
ControlAuthFile: filepath.Join(t.TempDir(), "sense-auth.json"),
|
|
ControlCursorKeyFile: filepath.Join(t.TempDir(), "sense-cursor.key"),
|
|
ConsoleEnabled: true, ConsoleWebRTCBaseURL: "http://127.0.0.1:8889",
|
|
}
|
|
if err := base.Validate(); err != nil {
|
|
t.Fatalf("valid loopback console rejected: %v", err)
|
|
}
|
|
withoutControl := base
|
|
withoutControl.ControlAPIEnabled = false
|
|
if err := withoutControl.Validate(); err == nil {
|
|
t.Fatal("console without Control API was accepted")
|
|
}
|
|
remoteBind := base
|
|
remoteBind.HTTPAddress, remoteBind.AllowNonLoopback = "0.0.0.0:8080", true
|
|
remoteBind.ControlAllowInsecureHTTP = true
|
|
if err := remoteBind.Validate(); err == nil {
|
|
t.Fatal("console on non-loopback Sense listener was accepted")
|
|
}
|
|
for _, invalidURL := range []string{
|
|
"http://media.example:8889", "ftp://127.0.0.1:8889", "http://127.0.0.1:8889/path",
|
|
"http://127.0.0.1:8889?token=hidden", "http://user@127.0.0.1:8889",
|
|
} {
|
|
candidate := base
|
|
candidate.ConsoleWebRTCBaseURL = invalidURL
|
|
if err := candidate.Validate(); err == nil {
|
|
t.Fatalf("invalid console WebRTC URL was accepted: %s", invalidURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateAuditRelaySecurityBoundary(t *testing.T) {
|
|
base := Config{
|
|
HTTPAddress: "127.0.0.1:8080", DatabaseDriver: "postgres",
|
|
DatabaseDSN: "postgres://sense-runtime@127.0.0.1/yovision?sslmode=disable",
|
|
MediaMTXURL: "http://127.0.0.1:9997", ReconcileInterval: time.Second, ProbeInterval: time.Second,
|
|
AuditRelayEnabled: true, AuditRelayURL: "http://127.0.0.1:8081/internal/v1/audit-events:batch",
|
|
AuditRelayKeyFile: filepath.Join(t.TempDir(), "relay-keys.json"), AuditRelayKeyID: "sense-a", AuditRelayInterval: time.Second,
|
|
}
|
|
if err := base.Validate(); err != nil {
|
|
t.Fatalf("valid loopback relay rejected: %v", err)
|
|
}
|
|
remoteHTTP := base
|
|
remoteHTTP.AuditRelayURL = "http://bell.example/internal/v1/audit-events:batch"
|
|
if err := remoteHTTP.Validate(); err == nil {
|
|
t.Fatal("remote plaintext relay was accepted")
|
|
}
|
|
sqlite := base
|
|
sqlite.DatabaseDriver, sqlite.DatabaseDSN = "sqlite", "file:test.db"
|
|
if err := sqlite.Validate(); err == nil {
|
|
t.Fatal("SQLite audit relay was accepted")
|
|
}
|
|
relativeKey := base
|
|
relativeKey.AuditRelayKeyFile = "relay-keys.json"
|
|
if err := relativeKey.Validate(); err == nil {
|
|
t.Fatal("repository-relative relay key was accepted")
|
|
}
|
|
}
|