test(chat,playout): enable test suite, coverage gate, switch to pure-Go SQLite driver
This commit is contained in:
1 parent
b3a10ba3ef
commit
e8a43b8a79
16 files changed
+3257
-8
No files matched your search
@@ -0,0 +1,947 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user