package handlers import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "sync" "testing" "time" "github.com/glebarez/sqlite" "gorm.io/gorm" "chat/src/auth" "chat/src/config" "chat/src/db" "chat/src/ws" ) // mockRedis implements db.RedisClient for handler tests. type mockRedis struct { mu sync.Mutex store map[string]string } func newMockRedis() *mockRedis { return &mockRedis{store: make(map[string]string)} } func (m *mockRedis) Ping(ctx context.Context) error { return nil } func (m *mockRedis) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error { m.mu.Lock() defer m.mu.Unlock() m.store[key] = value.(string) return nil } func (m *mockRedis) Get(ctx context.Context, key string) (string, error) { m.mu.Lock() defer m.mu.Unlock() return m.store[key], nil } func (m *mockRedis) Del(ctx context.Context, keys ...string) error { m.mu.Lock() defer m.mu.Unlock() for _, k := range keys { delete(m.store, k) } return nil } func (m *mockRedis) Incr(ctx context.Context, key string) (int64, error) { m.mu.Lock() defer m.mu.Unlock() m.store[key] = "1" return 1, nil } func (m *mockRedis) Decr(ctx context.Context, key string) (int64, error) { return 0, nil } func (m *mockRedis) Expire(ctx context.Context, key string, expiration time.Duration) error { return nil } func (m *mockRedis) Exists(ctx context.Context, keys ...string) (int64, error) { m.mu.Lock() defer m.mu.Unlock() for _, k := range keys { if _, ok := m.store[k]; ok { return 1, nil } } return 0, nil } func (m *mockRedis) Publish(ctx context.Context, channel string, message interface{}) error { return nil } func (m *mockRedis) PSubscribe(ctx context.Context, channels ...string) db.PubSub { return nil } func (m *mockRedis) Scan(ctx context.Context, cursor uint64, match string, count int64) ([]string, uint64, error) { m.mu.Lock() defer m.mu.Unlock() var keys []string for k := range m.store { if strings.Contains(k, strings.TrimSuffix(match, "*")) { keys = append(keys, k) } } return keys, 0, nil } func (m *mockRedis) Info(ctx context.Context, sections ...string) (string, error) { return "connected_clients:1\nused_memory_human:1M\n", nil } func (m *mockRedis) Close() error { return nil } func setupTestDeps(t *testing.T) (*Dependencies, *gorm.DB, *auth.SessionStore) { t.Helper() gormDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatalf("failed to open test DB: %v", err) } gormDB.AutoMigrate(&db.Message{}, &db.ModerationLog{}, &db.Media{}, &db.ScheduleOverride{}) mr := newMockRedis() sessionStore := auth.NewSessionStore() hub := ws.NewHub(gormDB, mr, "test-instance", 10, 10*time.Second, config.AdminUsername()) deps := &Dependencies{ DB: gormDB, Redis: mr, Hub: hub, SessionStore: sessionStore, DBDriver: "sqlite", } return deps, gormDB, sessionStore } // adminCookie creates a valid admin session and returns the cookie header. func adminCookie(t *testing.T, store *auth.SessionStore) string { t.Helper() token, _ := auth.GenerateToken() store.Add(token, time.Now().Add(1*time.Hour)) return config.AdminSessionCookieName + "=" + token } func TestHandleAdminLogin_Success(t *testing.T) { deps, _, _ := setupTestDeps(t) handler := HandleAdminLogin(deps) // Ensure env vars are set for credential check os.Setenv("ADMIN_USERNAME", "admin") os.Setenv("ADMIN_PASSWORD", "admin") defer os.Unsetenv("ADMIN_USERNAME") defer os.Unsetenv("ADMIN_PASSWORD") body := `{"username":"admin","password":"admin"}` req := httptest.NewRequest("POST", "/api/admin/login", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } var resp AdminSessionResponse json.NewDecoder(w.Body).Decode(&resp) if !resp.Authenticated { t.Error("Authenticated = false, want true") } // Check cookie set cookies := w.Result().Cookies() found := false for _, c := range cookies { if c.Name == config.AdminSessionCookieName { found = true break } } if !found { t.Error("session cookie not set") } } func TestHandleAdminLogin_InvalidCreds(t *testing.T) { deps, _, _ := setupTestDeps(t) handler := HandleAdminLogin(deps) body := `{"username":"admin","password":"wrong"}` req := httptest.NewRequest("POST", "/api/admin/login", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("status = %d, want 401", w.Code) } } func TestHandleAdminLogin_InvalidJSON(t *testing.T) { deps, _, _ := setupTestDeps(t) handler := HandleAdminLogin(deps) req := httptest.NewRequest("POST", "/api/admin/login", strings.NewReader("not json")) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusBadRequest { t.Errorf("status = %d, want 400", w.Code) } } func TestHandleAdminLogin_WrongMethod(t *testing.T) { deps, _, _ := setupTestDeps(t) handler := HandleAdminLogin(deps) req := httptest.NewRequest("GET", "/api/admin/login", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusMethodNotAllowed { t.Errorf("status = %d, want 405", w.Code) } } func TestHandleAdminLogout(t *testing.T) { deps, _, store := setupTestDeps(t) handler := HandleAdminLogout(deps) cookie := adminCookie(t, store) req := httptest.NewRequest("POST", "/api/admin/logout", nil) req.Header.Set("Cookie", cookie) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } // Cookie should be cleared cookies := w.Result().Cookies() found := false for _, c := range cookies { if c.Name == config.AdminSessionCookieName && c.MaxAge == -1 { found = true break } } if !found { t.Error("session cookie not cleared") } } func TestHandleAdminSession_Authenticated(t *testing.T) { deps, _, store := setupTestDeps(t) handler := HandleAdminSession(deps) cookie := adminCookie(t, store) req := httptest.NewRequest("GET", "/api/admin/session", nil) req.Header.Set("Cookie", cookie) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } var resp AdminSessionResponse json.NewDecoder(w.Body).Decode(&resp) if !resp.Authenticated { t.Error("Authenticated = false, want true") } } func TestHandleAdminSession_Unauthenticated(t *testing.T) { deps, _, _ := setupTestDeps(t) handler := HandleAdminSession(deps) req := httptest.NewRequest("GET", "/api/admin/session", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("status = %d, want 401", w.Code) } } func TestHandleAdminOverview(t *testing.T) { deps, gormDB, _ := setupTestDeps(t) // Add some test data gormDB.Create(&db.Message{Room: "main", Username: "u1", Text: "hi", Timestamp: time.Now(), Type: "public"}) gormDB.Create(&db.ModerationLog{Action: "mute", Target: "u2", Moderator: "admin", Reason: "spam", Timestamp: time.Now()}) handler := HandleAdminOverview(deps) req := httptest.NewRequest("GET", "/api/admin/overview", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } var overview AdminOverview json.NewDecoder(w.Body).Decode(&overview) if overview.StoredMessages != 1 { t.Errorf("StoredMessages = %d, want 1", overview.StoredMessages) } if overview.ModerationActions != 1 { t.Errorf("ModerationActions = %d, want 1", overview.ModerationActions) } } func TestHandleAdminPlaylist_Get(t *testing.T) { deps, _, _ := setupTestDeps(t) // Create temp playlist tmpDir := t.TempDir() playlistPath := filepath.Join(tmpDir, "playlist.txt") os.WriteFile(playlistPath, []byte("file /videos/test.mp4\n"), 0644) os.Setenv("PLAYLIST_PATH", playlistPath) defer os.Unsetenv("PLAYLIST_PATH") handler := HandleAdminPlaylist(deps) req := httptest.NewRequest("GET", "/api/admin/playlist", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } var payload PlaylistPayload json.NewDecoder(w.Body).Decode(&payload) if !strings.Contains(payload.Content, "test.mp4") { t.Errorf("Content = %q, want test.mp4", payload.Content) } } func TestHandleAdminPlaylist_Put(t *testing.T) { deps, _, _ := setupTestDeps(t) tmpDir := t.TempDir() playlistPath := filepath.Join(tmpDir, "playlist.txt") os.Setenv("PLAYLIST_PATH", playlistPath) defer os.Unsetenv("PLAYLIST_PATH") handler := HandleAdminPlaylist(deps) body := `{"content":"file /videos/new.mp4"}` req := httptest.NewRequest("PUT", "/api/admin/playlist", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } // Verify file written data, _ := os.ReadFile(playlistPath) if !strings.Contains(string(data), "new.mp4") { t.Errorf("file content = %q, want new.mp4", string(data)) } } func TestHandleAdminModerationLogs(t *testing.T) { deps, gormDB, _ := setupTestDeps(t) gormDB.Create(&db.ModerationLog{Action: "mute", Target: "u1", Moderator: "admin", Reason: "spam", Timestamp: time.Now()}) handler := HandleAdminModerationLogs(deps) req := httptest.NewRequest("GET", "/api/admin/moderation/logs", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } var resp ModerationLogsResponse json.NewDecoder(w.Body).Decode(&resp) if len(resp.Logs) != 1 { t.Errorf("len(Logs) = %d, want 1", len(resp.Logs)) } } func TestHandleAdminModerationAction(t *testing.T) { deps, gormDB, _ := setupTestDeps(t) handler := HandleAdminModerationAction(deps) body := `{"action":"mute","target":"baduser","moderator":"admin","reason":"spam","room":"main"}` req := httptest.NewRequest("POST", "/api/admin/moderation/actions", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } var count int64 gormDB.Model(&db.ModerationLog{}).Count(&count) if count != 1 { t.Errorf("moderation log count = %d, want 1", count) } } func TestHandleAdminModerationAction_MissingFields(t *testing.T) { deps, _, _ := setupTestDeps(t) handler := HandleAdminModerationAction(deps) body := `{"action":"mute"}` req := httptest.NewRequest("POST", "/api/admin/moderation/actions", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusBadRequest { t.Errorf("status = %d, want 400", w.Code) } } func TestHandleAdminAnalytics(t *testing.T) { deps, gormDB, _ := setupTestDeps(t) gormDB.Create(&db.Message{Room: "main", Username: "u1", Text: "hi", Timestamp: time.Now(), Type: "public"}) gormDB.Create(&db.Message{Room: "main", Username: "u2", Text: "hey", Timestamp: time.Now(), Type: "public"}) handler := HandleAdminAnalytics(deps) req := httptest.NewRequest("GET", "/api/admin/analytics", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } var analytics AdminAnalytics json.NewDecoder(w.Body).Decode(&analytics) if analytics.TotalMessages != 2 { t.Errorf("TotalMessages = %d, want 2", analytics.TotalMessages) } } func TestHandleAdminSystemSummary(t *testing.T) { deps, _, _ := setupTestDeps(t) handler := HandleAdminSystemSummary(deps) req := httptest.NewRequest("GET", "/api/admin/system", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } var summary AdminSystemSummary json.NewDecoder(w.Body).Decode(&summary) if summary.DatabaseDriver != "sqlite" { t.Errorf("DatabaseDriver = %q, want sqlite", summary.DatabaseDriver) } } func TestHandleAdminConfig_Get(t *testing.T) { deps, _, _ := setupTestDeps(t) handler := HandleAdminConfig(deps) req := httptest.NewRequest("GET", "/api/admin/config", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } } func TestHandleAdminConfig_Put(t *testing.T) { deps, _, _ := setupTestDeps(t) handler := HandleAdminConfig(deps) body := `{"rateLimitMax":20}` req := httptest.NewRequest("PUT", "/api/admin/config", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } } func TestHandleAdminRestart(t *testing.T) { deps, _, _ := setupTestDeps(t) handler := HandleAdminRestart(deps) req := httptest.NewRequest("POST", "/api/admin/restart?service=chat", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } } func TestHandleAdminUsers_Get(t *testing.T) { deps, _, _ := setupTestDeps(t) handler := HandleAdminUsers(deps) req := httptest.NewRequest("GET", "/api/admin/users", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } } func TestHandleAdminUsers_Post(t *testing.T) { deps, _, _ := setupTestDeps(t) handler := HandleAdminUsers(deps) body := `{"action":"mute","username":"baduser","duration":10}` req := httptest.NewRequest("POST", "/api/admin/users", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } } func TestHandleAdminSchedule(t *testing.T) { deps, gormDB, _ := setupTestDeps(t) tmpDir := t.TempDir() playlistPath := filepath.Join(tmpDir, "playlist.txt") os.WriteFile(playlistPath, []byte("file /videos/test.mp4\n"), 0644) os.Setenv("PLAYLIST_PATH", playlistPath) defer os.Unsetenv("PLAYLIST_PATH") gormDB.Create(&db.Media{ Filename: "test.mp4", OriginalName: "test.mp4", Path: "/videos/test.mp4", SizeBytes: 1024, DurationSeconds: 60, Status: "ready", Title: "Test Video", }) handler := HandleAdminSchedule(deps) req := httptest.NewRequest("GET", "/api/admin/schedule", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } var resp AdminScheduleResponse json.NewDecoder(w.Body).Decode(&resp) if resp.MatchedEntries != 1 { t.Errorf("MatchedEntries = %d, want 1", resp.MatchedEntries) } } func TestHandleAdminScheduleOverrides_CRUD(t *testing.T) { deps, gormDB, _ := setupTestDeps(t) // Create handler := HandleAdminScheduleOverrides(deps) now := time.Now() body, _ := json.Marshal(db.ScheduleOverride{ MediaID: 1, StartTime: now, EndTime: now.Add(1 * time.Hour), Reason: "test", }) req := httptest.NewRequest("POST", "/api/admin/schedule/overrides", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusCreated { t.Errorf("POST status = %d, want 201", w.Code) } // List req2 := httptest.NewRequest("GET", "/api/admin/schedule/overrides", nil) w2 := httptest.NewRecorder() handler(w2, req2) if w2.Code != http.StatusOK { t.Errorf("GET status = %d, want 200", w2.Code) } // Delete var overrides []db.ScheduleOverride gormDB.Find(&overrides) if len(overrides) > 0 { req3 := httptest.NewRequest("DELETE", "/api/admin/schedule/overrides?id=1", nil) w3 := httptest.NewRecorder() handler(w3, req3) if w3.Code != http.StatusNoContent { t.Errorf("DELETE status = %d, want 204", w3.Code) } } } func TestHandleAdminChatLog(t *testing.T) { deps, gormDB, _ := setupTestDeps(t) gormDB.Create(&db.Message{Room: "main", Username: "u1", Text: "hello", Timestamp: time.Now(), Type: "public"}) handler := HandleAdminChatLog(deps) req := httptest.NewRequest("GET", "/api/admin/chat-log", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } var resp ChatLogListResponse json.NewDecoder(w.Body).Decode(&resp) if resp.Total != 1 { t.Errorf("Total = %d, want 1", resp.Total) } } func TestHandleAdminChatLog_FilterByRoom(t *testing.T) { deps, gormDB, _ := setupTestDeps(t) gormDB.Create(&db.Message{Room: "main", Username: "u1", Text: "hello", Timestamp: time.Now(), Type: "public"}) gormDB.Create(&db.Message{Room: "lobby", Username: "u2", Text: "hey", Timestamp: time.Now(), Type: "public"}) handler := HandleAdminChatLog(deps) req := httptest.NewRequest("GET", "/api/admin/chat-log?room=main", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } var resp ChatLogListResponse json.NewDecoder(w.Body).Decode(&resp) if resp.Total != 1 { t.Errorf("Total = %d, want 1", resp.Total) } } func TestHandleAdminChatLogByID_Get(t *testing.T) { deps, gormDB, _ := setupTestDeps(t) msg := db.Message{Room: "main", Username: "u1", Text: "hello", Timestamp: time.Now(), Type: "public"} gormDB.Create(&msg) handler := HandleAdminChatLogByID(deps) req := httptest.NewRequest("GET", "/api/admin/chat-log/1", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } } func TestHandleAdminChatLogByID_Delete(t *testing.T) { deps, gormDB, _ := setupTestDeps(t) msg := db.Message{Room: "main", Username: "u1", Text: "hello", Timestamp: time.Now(), Type: "public"} gormDB.Create(&msg) handler := HandleAdminChatLogByID(deps) req := httptest.NewRequest("DELETE", "/api/admin/chat-log/1", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusNoContent { t.Errorf("status = %d, want 204", w.Code) } var count int64 gormDB.Model(&db.Message{}).Count(&count) if count != 0 { t.Errorf("message count after delete = %d, want 0", count) } } func TestHandleAdminChatLogBulk(t *testing.T) { deps, gormDB, _ := setupTestDeps(t) gormDB.Create(&db.Message{Room: "main", Username: "u1", Text: "m1", Timestamp: time.Now(), Type: "public"}) gormDB.Create(&db.Message{Room: "main", Username: "u2", Text: "m2", Timestamp: time.Now(), Type: "public"}) handler := HandleAdminChatLogBulk(deps) body := `{"ids":[1,2]}` req := httptest.NewRequest("DELETE", "/api/admin/chat-log/bulk", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } var count int64 gormDB.Model(&db.Message{}).Count(&count) if count != 0 { t.Errorf("message count after bulk delete = %d, want 0", count) } } func TestHandleAdminChatLogExport(t *testing.T) { deps, gormDB, _ := setupTestDeps(t) gormDB.Create(&db.Message{Room: "main", Username: "u1", Text: "hello", Timestamp: time.Now(), Type: "public"}) handler := HandleAdminChatLogExport(deps) req := httptest.NewRequest("GET", "/api/admin/chat-log/export", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } contentType := w.Header().Get("Content-Type") if !strings.Contains(contentType, "text/csv") { t.Errorf("Content-Type = %q, want text/csv", contentType) } body := w.Body.String() if !strings.Contains(body, "hello") { t.Errorf("CSV body missing 'hello': %s", body) } } func TestHandlePublicSchedule(t *testing.T) { deps, _, _ := setupTestDeps(t) tmpDir := t.TempDir() playlistPath := filepath.Join(tmpDir, "playlist.txt") os.WriteFile(playlistPath, []byte("file /videos/test.mp4\n"), 0644) os.Setenv("PLAYLIST_PATH", playlistPath) defer os.Unsetenv("PLAYLIST_PATH") handler := HandlePublicSchedule(deps) req := httptest.NewRequest("GET", "/api/schedule", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } } func TestHandleAdminMedia_Get(t *testing.T) { deps, gormDB, _ := setupTestDeps(t) gormDB.Create(&db.Media{ Filename: "test.mp4", OriginalName: "test.mp4", Path: "/media/test.mp4", SizeBytes: 1024, DurationSeconds: 60, Status: "ready", Title: "Test", }) handler := HandleAdminMedia(deps) req := httptest.NewRequest("GET", "/api/admin/media", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } var resp AdminMediaResponse json.NewDecoder(w.Body).Decode(&resp) if len(resp.Files) != 1 { t.Errorf("len(Files) = %d, want 1", len(resp.Files)) } } func TestHandleAdminMediaByID_Get(t *testing.T) { deps, gormDB, _ := setupTestDeps(t) gormDB.Create(&db.Media{ Filename: "test.mp4", OriginalName: "test.mp4", Path: "/media/test.mp4", SizeBytes: 1024, DurationSeconds: 60, Status: "ready", Title: "Test", }) handler := HandleAdminMediaByID(deps) req := httptest.NewRequest("GET", "/api/admin/media/1", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } } func TestHandleAdminMediaByID_NotFound(t *testing.T) { deps, _, _ := setupTestDeps(t) handler := HandleAdminMediaByID(deps) req := httptest.NewRequest("GET", "/api/admin/media/999", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusNotFound { t.Errorf("status = %d, want 404", w.Code) } } func TestHandleAdminMediaByID_Put(t *testing.T) { deps, gormDB, _ := setupTestDeps(t) gormDB.Create(&db.Media{ Filename: "test.mp4", OriginalName: "test.mp4", Path: "/media/test.mp4", SizeBytes: 1024, DurationSeconds: 60, Status: "ready", Title: "Old Title", }) handler := HandleAdminMediaByID(deps) body := `{"title":"New Title","status":"needs_normalization","tags":"tag1"}` req := httptest.NewRequest("PUT", "/api/admin/media/1", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } var updated db.Media gormDB.First(&updated, 1) if updated.Title != "New Title" { t.Errorf("Title = %q, want New Title", updated.Title) } } func TestHandleAdminMediaByID_Delete(t *testing.T) { deps, gormDB, _ := setupTestDeps(t) // Create a real temp file for the media record tmpDir := t.TempDir() os.Setenv("MEDIA_DIR", tmpDir) defer os.Unsetenv("MEDIA_DIR") mediaPath := filepath.Join(tmpDir, "test.mp4") os.WriteFile(mediaPath, []byte("fake video"), 0644) gormDB.Create(&db.Media{ Filename: "test.mp4", OriginalName: "test.mp4", Path: mediaPath, SizeBytes: 10, DurationSeconds: 1, Status: "ready", Title: "Test", }) handler := HandleAdminMediaByID(deps) req := httptest.NewRequest("DELETE", "/api/admin/media/1", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } var count int64 gormDB.Model(&db.Media{}).Count(&count) if count != 0 { t.Errorf("media count after delete = %d, want 0", count) } } func TestHandleAdminMediaUploads_Init(t *testing.T) { deps, _, _ := setupTestDeps(t) handler := HandleAdminMediaUploads(deps) body := `{"fileName":"video.mp4","fileSize":1024,"mimeType":"video/mp4","chunkSize":524288,"totalChunks":1}` req := httptest.NewRequest("POST", "/api/admin/media/uploads", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } var status AdminMediaUploadStatusResponse json.NewDecoder(w.Body).Decode(&status) if status.FileName != "video.mp4" { t.Errorf("FileName = %q, want video.mp4", status.FileName) } if status.UploadID == "" { t.Error("UploadID is empty") } } func TestHandleAdminMediaUploads_InvalidFile(t *testing.T) { deps, _, _ := setupTestDeps(t) handler := HandleAdminMediaUploads(deps) body := `{"fileName":"bad.exe","fileSize":1024,"mimeType":"application/x-msdownload","chunkSize":524288,"totalChunks":1}` req := httptest.NewRequest("POST", "/api/admin/media/uploads", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusBadRequest { t.Errorf("status = %d, want 400", w.Code) } }