19eddba1ee
Changes: - RTSP now generates single URL based on credentials availability * With credentials: only rtsp://user:pass@host/path * Without credentials: only rtsp://host/path - HTTP/HTTPS unchanged: still generates 4 auth variants - Improved deduplication efficiency from 66% to 100% for RTSP - Added comprehensive test coverage for protocol auth behavior This reduces unnecessary stream testing and improves discovery speed
352 lines
8.3 KiB
Go
352 lines
8.3 KiB
Go
package stream
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/eduard256/Strix/internal/models"
|
|
)
|
|
|
|
// TestProtocolAuthBehaviorComparison проверяет разницу в генерации auth вариантов
|
|
// между RTSP и HTTP протоколами
|
|
func TestProtocolAuthBehaviorComparison(t *testing.T) {
|
|
logger := &mockLogger{}
|
|
builder := NewBuilder([]string{}, logger)
|
|
|
|
ctx := BuildContext{
|
|
IP: "192.168.1.100",
|
|
Username: "admin",
|
|
Password: "12345",
|
|
Port: 0, // Will use default for protocol
|
|
}
|
|
|
|
t.Log("\n" + strings.Repeat("=", 80))
|
|
t.Log("PROTOCOL AUTH BEHAVIOR COMPARISON")
|
|
t.Log(strings.Repeat("=", 80))
|
|
|
|
// === RTSP ===
|
|
t.Log("\n### RTSP Protocol ###")
|
|
rtspEntry := models.CameraEntry{
|
|
Type: "FFMPEG",
|
|
Protocol: "rtsp",
|
|
Port: 554,
|
|
URL: "/live/ch0",
|
|
}
|
|
|
|
rtspURLs := builder.BuildURLsFromEntry(rtspEntry, ctx)
|
|
|
|
t.Logf("\nRTSP with credentials (user=%s, pass=%s):", "admin", "***")
|
|
t.Logf("Generated: %d URL(s)", len(rtspURLs))
|
|
for i, url := range rtspURLs {
|
|
t.Logf(" [%d] %s", i+1, url)
|
|
}
|
|
|
|
// Check RTSP behavior
|
|
if len(rtspURLs) != 1 {
|
|
t.Errorf("❌ RTSP: Expected 1 URL, got %d", len(rtspURLs))
|
|
}
|
|
|
|
hasRTSPAuth := false
|
|
hasRTSPNoAuth := false
|
|
for _, url := range rtspURLs {
|
|
if strings.Contains(url, "@") {
|
|
hasRTSPAuth = true
|
|
} else {
|
|
hasRTSPNoAuth = true
|
|
}
|
|
}
|
|
|
|
if !hasRTSPAuth {
|
|
t.Error("❌ RTSP: Should have URL WITH auth")
|
|
}
|
|
if hasRTSPNoAuth {
|
|
t.Error("❌ RTSP: Should NOT have URL without auth when credentials provided")
|
|
}
|
|
|
|
if len(rtspURLs) == 1 && hasRTSPAuth && !hasRTSPNoAuth {
|
|
t.Log("✅ RTSP: Correctly generates ONLY auth URL")
|
|
}
|
|
|
|
// === HTTP ===
|
|
t.Log("\n### HTTP Protocol ###")
|
|
httpEntry := models.CameraEntry{
|
|
Type: "JPEG",
|
|
Protocol: "http",
|
|
Port: 80,
|
|
URL: "snapshot.cgi",
|
|
}
|
|
|
|
httpURLs := builder.BuildURLsFromEntry(httpEntry, ctx)
|
|
|
|
t.Logf("\nHTTP with credentials (user=%s, pass=%s):", "admin", "***")
|
|
t.Logf("Generated: %d URL(s)", len(httpURLs))
|
|
for i, url := range httpURLs {
|
|
t.Logf(" [%d] %s", i+1, url)
|
|
}
|
|
|
|
// Check HTTP behavior
|
|
if len(httpURLs) != 4 {
|
|
t.Errorf("❌ HTTP: Expected 4 URLs, got %d", len(httpURLs))
|
|
}
|
|
|
|
// Analyze HTTP URLs
|
|
type authVariant struct {
|
|
name string
|
|
found bool
|
|
url string
|
|
}
|
|
|
|
variants := []authVariant{
|
|
{name: "No auth", found: false},
|
|
{name: "Basic auth only", found: false},
|
|
{name: "Query params only", found: false},
|
|
{name: "Basic auth + Query params", found: false},
|
|
}
|
|
|
|
for _, url := range httpURLs {
|
|
hasBasicAuth := strings.Contains(url, "@")
|
|
hasQueryParams := strings.Contains(url, "?")
|
|
|
|
if !hasBasicAuth && !hasQueryParams {
|
|
variants[0].found = true
|
|
variants[0].url = url
|
|
} else if hasBasicAuth && !hasQueryParams {
|
|
variants[1].found = true
|
|
variants[1].url = url
|
|
} else if !hasBasicAuth && hasQueryParams {
|
|
variants[2].found = true
|
|
variants[2].url = url
|
|
} else if hasBasicAuth && hasQueryParams {
|
|
variants[3].found = true
|
|
variants[3].url = url
|
|
}
|
|
}
|
|
|
|
t.Log("\nHTTP Auth variants breakdown:")
|
|
allFound := true
|
|
for i, v := range variants {
|
|
if v.found {
|
|
t.Logf(" ✅ [%d] %s: %s", i+1, v.name, v.url)
|
|
} else {
|
|
t.Errorf(" ❌ [%d] %s: MISSING", i+1, v.name)
|
|
allFound = false
|
|
}
|
|
}
|
|
|
|
if allFound {
|
|
t.Log("\n✅ HTTP: Correctly generates ALL 4 auth variants")
|
|
} else {
|
|
t.Error("\n❌ HTTP: Missing some auth variants")
|
|
}
|
|
|
|
// === COMPARISON SUMMARY ===
|
|
t.Log("\n" + strings.Repeat("=", 80))
|
|
t.Log("SUMMARY")
|
|
t.Log(strings.Repeat("=", 80))
|
|
t.Log("\nRTSP behavior:")
|
|
t.Log(" • With credentials → 1 URL (WITH auth only)")
|
|
t.Log(" • Without credentials → 1 URL (NO auth only)")
|
|
t.Log(" • Rationale: RTSP auth is binary (works or doesn't)")
|
|
t.Log("")
|
|
t.Log("HTTP behavior:")
|
|
t.Log(" • With credentials → 4 URLs:")
|
|
t.Log(" 1. No auth (try public access)")
|
|
t.Log(" 2. Basic auth only (user:pass@host)")
|
|
t.Log(" 3. Query params only (?user=X&pwd=Y)")
|
|
t.Log(" 4. Both methods combined")
|
|
t.Log(" • Rationale: Different cameras support different auth methods")
|
|
t.Log(strings.Repeat("=", 80))
|
|
}
|
|
|
|
// TestRTSPNoAuthWhenNoCredentials проверяет что RTSP без credentials НЕ генерирует auth URL
|
|
func TestRTSPNoAuthWhenNoCredentials(t *testing.T) {
|
|
logger := &mockLogger{}
|
|
builder := NewBuilder([]string{}, logger)
|
|
|
|
rtspEntry := models.CameraEntry{
|
|
Type: "FFMPEG",
|
|
Protocol: "rtsp",
|
|
Port: 554,
|
|
URL: "/live/main",
|
|
}
|
|
|
|
// Without credentials
|
|
ctxNoAuth := BuildContext{
|
|
IP: "192.168.1.100",
|
|
Username: "",
|
|
Password: "",
|
|
Port: 554,
|
|
}
|
|
|
|
urls := builder.BuildURLsFromEntry(rtspEntry, ctxNoAuth)
|
|
|
|
t.Log("\n=== RTSP WITHOUT credentials ===")
|
|
t.Logf("Generated: %d URL(s)", len(urls))
|
|
for i, url := range urls {
|
|
t.Logf(" [%d] %s", i+1, url)
|
|
}
|
|
|
|
if len(urls) != 1 {
|
|
t.Errorf("Expected 1 URL, got %d", len(urls))
|
|
}
|
|
|
|
if len(urls) > 0 {
|
|
if strings.Contains(urls[0], "@") {
|
|
t.Error("❌ Should NOT have auth when no credentials provided")
|
|
} else {
|
|
t.Log("✅ Correctly generates URL without auth")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestHTTPNoAuthWhenNoCredentials проверяет что HTTP без credentials генерирует ТОЛЬКО 1 URL
|
|
func TestHTTPNoAuthWhenNoCredentials(t *testing.T) {
|
|
logger := &mockLogger{}
|
|
builder := NewBuilder([]string{}, logger)
|
|
|
|
httpEntry := models.CameraEntry{
|
|
Type: "JPEG",
|
|
Protocol: "http",
|
|
Port: 80,
|
|
URL: "snapshot.jpg",
|
|
}
|
|
|
|
// Without credentials
|
|
ctxNoAuth := BuildContext{
|
|
IP: "192.168.1.100",
|
|
Username: "",
|
|
Password: "",
|
|
Port: 80,
|
|
}
|
|
|
|
urls := builder.BuildURLsFromEntry(httpEntry, ctxNoAuth)
|
|
|
|
t.Log("\n=== HTTP WITHOUT credentials ===")
|
|
t.Logf("Generated: %d URL(s)", len(urls))
|
|
for i, url := range urls {
|
|
t.Logf(" [%d] %s", i+1, url)
|
|
}
|
|
|
|
if len(urls) != 1 {
|
|
t.Errorf("Expected 1 URL, got %d", len(urls))
|
|
}
|
|
|
|
if len(urls) > 0 {
|
|
if strings.Contains(urls[0], "@") || strings.Contains(urls[0], "?") {
|
|
t.Error("❌ Should NOT have auth when no credentials provided")
|
|
} else {
|
|
t.Log("✅ Correctly generates URL without auth")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCompleteProtocolMatrix проверяет полную матрицу протоколов и credentials
|
|
func TestCompleteProtocolMatrix(t *testing.T) {
|
|
logger := &mockLogger{}
|
|
builder := NewBuilder([]string{}, logger)
|
|
|
|
type testCase struct {
|
|
protocol string
|
|
port int
|
|
url string
|
|
withCreds bool
|
|
expectedURLs int
|
|
description string
|
|
}
|
|
|
|
tests := []testCase{
|
|
// RTSP
|
|
{
|
|
protocol: "rtsp",
|
|
port: 554,
|
|
url: "/live/ch0",
|
|
withCreds: true,
|
|
expectedURLs: 1,
|
|
description: "RTSP with credentials",
|
|
},
|
|
{
|
|
protocol: "rtsp",
|
|
port: 554,
|
|
url: "/live/ch0",
|
|
withCreds: false,
|
|
expectedURLs: 1,
|
|
description: "RTSP without credentials",
|
|
},
|
|
// HTTP
|
|
{
|
|
protocol: "http",
|
|
port: 80,
|
|
url: "snapshot.cgi",
|
|
withCreds: true,
|
|
expectedURLs: 4,
|
|
description: "HTTP with credentials",
|
|
},
|
|
{
|
|
protocol: "http",
|
|
port: 80,
|
|
url: "snapshot.cgi",
|
|
withCreds: false,
|
|
expectedURLs: 1,
|
|
description: "HTTP without credentials",
|
|
},
|
|
// HTTPS
|
|
{
|
|
protocol: "https",
|
|
port: 443,
|
|
url: "snapshot.cgi",
|
|
withCreds: true,
|
|
expectedURLs: 4,
|
|
description: "HTTPS with credentials",
|
|
},
|
|
{
|
|
protocol: "https",
|
|
port: 443,
|
|
url: "snapshot.cgi",
|
|
withCreds: false,
|
|
expectedURLs: 1,
|
|
description: "HTTPS without credentials",
|
|
},
|
|
}
|
|
|
|
t.Log("\n" + strings.Repeat("=", 80))
|
|
t.Log("COMPLETE PROTOCOL MATRIX")
|
|
t.Log(strings.Repeat("=", 80))
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
entry := models.CameraEntry{
|
|
Type: "FFMPEG",
|
|
Protocol: tc.protocol,
|
|
Port: tc.port,
|
|
URL: tc.url,
|
|
}
|
|
|
|
ctx := BuildContext{
|
|
IP: "192.168.1.100",
|
|
Port: tc.port,
|
|
}
|
|
|
|
if tc.withCreds {
|
|
ctx.Username = "admin"
|
|
ctx.Password = "12345"
|
|
}
|
|
|
|
urls := builder.BuildURLsFromEntry(entry, ctx)
|
|
|
|
t.Logf("Protocol: %s, Creds: %v → Generated: %d URL(s)",
|
|
tc.protocol, tc.withCreds, len(urls))
|
|
|
|
if len(urls) != tc.expectedURLs {
|
|
t.Errorf("❌ Expected %d URLs, got %d", tc.expectedURLs, len(urls))
|
|
for i, url := range urls {
|
|
t.Logf(" [%d] %s", i+1, url)
|
|
}
|
|
} else {
|
|
t.Logf("✅ Correct: %d URL(s)", len(urls))
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Log(strings.Repeat("=", 80))
|
|
}
|