package api
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
"github.com/paul/kiss-media-player/internal"
"github.com/paul/kiss-media-player/internal/auth"
"github.com/paul/kiss-media-player/internal/clock"
"github.com/paul/kiss-media-player/internal/model"
"github.com/paul/kiss-media-player/internal/repository"
"github.com/paul/kiss-media-player/internal/service"
)
// ------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------
func makeTempFile(t *testing.T, data string) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "media-*.mp4")
if err != nil {
t.Fatal(err)
}
defer f.Close()
if _, err := f.WriteString(data); err != nil {
t.Fatal(err)
}
return f.Name()
}
func newUploadRequest(t *testing.T, setID, filename, content string) *http.Request {
t.Helper()
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
part, err := w.CreateFormFile("file", filename)
if err != nil {
t.Fatal(err)
}
_, _ = io.WriteString(part, content)
_ = w.Close()
req := httptest.NewRequest(http.MethodPost, "/api/sets/"+setID+"/upload", &buf)
req.Header.Set("Content-Type", w.FormDataContentType())
return req
}
func buildAdminSessionStore(userID int64) *repository.MockStore {
store := buildSessionStore(userID)
store.UserRepo.GetUserByIDFunc = func(ctx context.Context, id int64) (*model.User, error) {
return &model.User{ID: id, Username: "admin", IsAdmin: true}, nil
}
return store
}
func sessionCookieForStore(t *testing.T, store repository.Store, sm *auth.SessionManager, userID int64) *http.Cookie {
t.Helper()
return addSessionCookie(t, store, sm, userID)
}
// ------------------------------------------------------------------
// Server helpers (server.go)
// ------------------------------------------------------------------
func Test_addrFromPort(t *testing.T) {
if got := addrFromPort(8080); got != ":8080" {
t.Fatalf("expected :8080, got %s", got)
}
}
func TestNewGracefulServer(t *testing.T) {
cfg := &internal.Config{Port: 3000}
gs := NewGracefulServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}), cfg)
if gs.Server.Addr != ":3000" {
t.Fatalf("unexpected addr %s", gs.Server.Addr)
}
}
func TestPingStore_nonPinger(t *testing.T) {
store := &repository.MockStore{}
srv := newTestServer(t, store, nil, nil, &internal.Config{}, nil, nil, nil, nil)
if err := srv.pingStore(context.Background()); err != nil {
t.Fatal("expected nil for non-pinger")
}
}
func TestPingStore_pingerError(t *testing.T) {
store := &mockPingStore{err: errors.New("down")}
srv := newTestServer(t, store, nil, nil, &internal.Config{}, nil, nil, nil, nil)
if err := srv.pingStore(context.Background()); err == nil {
t.Fatal("expected error")
}
}
// ------------------------------------------------------------------
// String / readJSON / writeJSON / context helpers
// ------------------------------------------------------------------
func Test_stringPtr(t *testing.T) {
s := "x"
p := stringPtr(s)
if p == nil || *p != s {
t.Fatal("unexpected")
}
}
func Test_readJSON_nilBody(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Body = nil
var dst map[string]any
if err := readJSON(req, &dst); err == nil {
t.Fatal("expected error for nil body")
}
}
func Test_writeJSON_encodeError(t *testing.T) {
// channel cannot be JSON-encoded, triggering the error path
rr := httptest.NewRecorder()
writeJSON(rr, http.StatusOK, make(chan int))
if rr.Code != http.StatusOK {
t.Fatalf("expected %d, got %d", http.StatusOK, rr.Code)
}
}
func Test_userIDFromContext_missing(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
if userIDFromContext(req) != 0 {
t.Fatal("expected 0")
}
}
func Test_sessionIDFromContext_missing(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
if sessionIDFromContext(req) != "" {
t.Fatal("expected empty")
}
}
// ------------------------------------------------------------------
// Static pages (serveFile)
// ------------------------------------------------------------------
func TestServer_ServeFile_success(t *testing.T) {
store := buildSessionStore(1)
store.SessionRepo = repository.MockSessionRepo{
GetSessionByIDFunc: func(ctx context.Context, id string) (*model.Session, error) {
return &model.Session{ID: id, UserID: 1, ExpiresAt: time.Now().Add(time.Hour)}, nil
},
}
sm := auth.NewSessionManager(store, &clock.MockClock{T: time.Now()}, time.Hour)
srv := newTestServer(t, store, nil, sm, &internal.Config{SessionTimeoutHours: 24}, nil, nil, nil, nil)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(sessionCookieForStore(t, store, sm, 1))
rr := httptest.NewRecorder()
srv.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected %d, got %d", http.StatusOK, rr.Code)
}
}
func TestServer_ServeFile_notFound(t *testing.T) {
fs := newTestFS(map[string]string{}) // no index.html
store := buildSessionStore(1)
store.SessionRepo = repository.MockSessionRepo{
GetSessionByIDFunc: func(ctx context.Context, id string) (*model.Session, error) {
return &model.Session{ID: id, UserID: 1, ExpiresAt: time.Now().Add(time.Hour)}, nil
},
}
sm := auth.NewSessionManager(store, &clock.MockClock{T: time.Now()}, time.Hour)
srv := newTestServer(t, store, nil, sm, &internal.Config{SessionTimeoutHours: 24}, nil, nil, nil, fs)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(sessionCookieForStore(t, store, sm, 1))
rr := httptest.NewRecorder()
srv.ServeHTTP(rr, req)
if rr.Code != http.StatusNotFound {
t.Fatalf("expected %d, got %d", http.StatusNotFound, rr.Code)
}
}
// ------------------------------------------------------------------
// Bootstrap negative paths
// ------------------------------------------------------------------
func TestServer_Bootstrap_negativePaths(t *testing.T) {
tests := []struct {
name string
body string
countErr error
hashErr error
createErr error
sessErr error
wantCode int
}{
{"invalid json", `bad`, nil, nil, nil, nil, http.StatusBadRequest},
{"count users error", `{"username":"u","password":"p"}`, errors.New("boom"), nil, nil, nil, http.StatusInternalServerError},
{"hash error", `{"username":"u","password":"p"}`, nil, errors.New("boom"), nil, nil, http.StatusInternalServerError},
{"create user error", `{"
|