package liveview import ( "context" "encoding/json" "net/http/httptest" "strings" "testing" "time" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func testDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) if err != nil { t.Fatal(err) } statements := []string{ `CREATE TABLE sense_devices (id text primary key, name text, location text, status text)`, `CREATE TABLE sense_admission_profiles (device_id text, token text, name text, kind text, width integer, height integer, encoding text, verification_status text, stream_uri text)`, `CREATE TABLE sense_media_routes (id text primary key, device_id text, profile_token text, path text, desired text, actual text, detail text, readers integer)`, `INSERT INTO sense_devices VALUES ('device-1','东门摄像机','教学楼东门','active')`, `INSERT INTO sense_admission_profiles VALUES ('device-1','main','主码流','main',1920,1080,'H264','ready','rtsp://camera.example/live')`, `INSERT INTO sense_media_routes VALUES ('device-1:main','device-1','main','sense_012345','running','waiting','等待播放器连接并按需拉流',0)`, } for _, statement := range statements { if err = db.Exec(statement).Error; err != nil { t.Fatal(err) } } return db } func TestListIsPaginatedSearchableAndSecretFree(t *testing.T) { service := NewService(testDB(t), NewStore()) items, total, err := service.List(context.Background(), PageRequest{Keyword: "东门", PageIndex: 1, PageSize: 10}) if err != nil || total != 1 || len(items) != 1 || items[0].ProfileKind != "main" { t.Fatalf("items=%+v total=%d err=%v", items, total, err) } encoded, _ := json.Marshal(items) if strings.Contains(string(encoded), "rtsp://") || strings.Contains(string(encoded), "sense_012345") { t.Fatalf("response leaked private media data: %s", encoded) } } func TestSessionIsOwnerBoundShortLivedAndUsesBrowserHost(t *testing.T) { store := NewStore() now := time.Date(2026, 8, 14, 10, 0, 0, 0, time.UTC) store.now = func() time.Time { return now } service := NewService(testDB(t), store) request := httptest.NewRequest("POST", "http://192.0.2.20:18080/api/v1/liveview/sessions", nil) session, err := service.Create(context.Background(), 7, "device-1:main", request) if err != nil { t.Fatal(err) } if !strings.HasPrefix(session.PlayerURL, "/api/v1/liveview/player/view_") || strings.Contains(session.PlayerURL, "sense_012345") { t.Fatalf("unsafe player URL: %s", session.PlayerURL) } if _, err = service.Get(context.Background(), 8, session.ID); !errorsIs(err, ErrSessionExpired) { t.Fatalf("another owner accessed session: %v", err) } target, err := service.PlayerTarget(session.ID) if err != nil || target != "http://192.0.2.20:8889/sense_012345?controls=true&muted=true&autoplay=true" { t.Fatalf("target=%q err=%v", target, err) } now = now.Add(sessionTTL) if _, err = service.PlayerTarget(session.ID); !errorsIs(err, ErrSessionExpired) { t.Fatalf("expired session remained valid: %v", err) } } func TestPlaybackBaseRejectsCredentials(t *testing.T) { if _, err := validatePlaybackBase("http://invalid-user@127.0.0.1:8889"); err == nil { t.Fatal("expected credential-bearing base URL to be rejected") } } func TestPlaybackStatusKeepsActionableFailuresDistinct(t *testing.T) { for input, want := range map[string]string{"waiting": "waiting", "credential_unavailable": "authentication_failed", "path_missing": "stream_not_found", "process_unavailable": "service_unavailable", "unexpected": "offline"} { if got := playbackStatus(input); got != want { t.Fatalf("playbackStatus(%q)=%q want %q", input, got, want) } } } func errorsIs(err, target error) bool { return err == target }