package web import ( "encoding/json" "net/http" "net/http/httptest" "net/url" "path/filepath" "strings" "testing" "time" "github.com/domagojzecevic/cammonitor/internal/auth" "github.com/domagojzecevic/cammonitor/internal/config" "github.com/domagojzecevic/cammonitor/internal/db" "github.com/domagojzecevic/cammonitor/internal/footage" ) func TestHealthReturnsOK(t *testing.T) { router := NewRouter(nil, nil, nil) request := httptest.NewRequest(http.MethodGet, "/health", nil) response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("expected status %d, got %d", http.StatusOK, response.Code) } var body map[string]string if err := json.NewDecoder(response.Body).Decode(&body); err != nil { t.Fatalf("decode response body: %v", err) } if body["status"] != "ok" { t.Fatalf("expected status ok, got %q", body["status"]) } } func TestAdminUsersRedirectsWithoutSessionCookie(t *testing.T) { database, err := db.Open(filepath.Join(t.TempDir(), "cammonitor.db")) if err != nil { t.Fatalf("open database: %v", err) } t.Cleanup(func() { if err := database.Close(); err != nil { t.Fatalf("close database: %v", err) } }) router := NewRouter(&config.Config{SessionTTL: time.Hour}, database, nil) request := httptest.NewRequest(http.MethodGet, "/admin/users", nil) response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusFound { t.Fatalf("expected status %d, got %d", http.StatusFound, response.Code) } if location := response.Header().Get("Location"); location != "/login" { t.Fatalf("expected redirect to /login, got %q", location) } } func TestIndexRedirectsToNewestDayImages(t *testing.T) { router, cookie := newAuthenticatedRouter(t) request := httptest.NewRequest(http.MethodGet, "/", nil) request.AddCookie(cookie) response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusFound { t.Fatalf("expected status %d, got %d", http.StatusFound, response.Code) } if location := response.Header().Get("Location"); location != "/day/20260102/images" { t.Fatalf("expected redirect to newest day images, got %q", location) } } func TestDayOverviewRendersShellNavigationAndCounts(t *testing.T) { router, cookie := newAuthenticatedRouter(t) request := httptest.NewRequest(http.MethodGet, "/day/20260101", nil) request.AddCookie(cookie) response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("expected status %d, got %d", http.StatusOK, response.Code) } body := response.Body.String() for _, want := range []string{ `https://cdn.tailwindcss.com`, `class="hidden w-56 shrink-0 border-r border-slate-800 bg-slate-950 md:block"`, `data-mobile-drawer`, `class="fixed inset-x-0 bottom-0 z-20 grid grid-cols-2 border-t border-slate-800 bg-slate-950/95 md:hidden"`, `2026-01`, `href="/day/20260102"`, `href="/day/20260101"`, `Images (2)`, `Videos (2)`, `href="/day/20260101/images"`, `href="/day/20260101/videos"`, } { if !strings.Contains(body, want) { t.Fatalf("expected response to contain %q\nbody:\n%s", want, body) } } } func TestDayOverviewReturnsNotFoundForMissingDay(t *testing.T) { router, cookie := newAuthenticatedRouter(t) request := httptest.NewRequest(http.MethodGet, "/day/20260103", nil) request.AddCookie(cookie) response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusNotFound { t.Fatalf("expected status %d, got %d", http.StatusNotFound, response.Code) } } func TestImagePageRendersAuthenticatedBrowser(t *testing.T) { router, cookie := newAuthenticatedRouter(t) request := httptest.NewRequest(http.MethodGet, "/day/20260101/images?idx=1", nil) request.AddCookie(cookie) response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("expected status %d, got %d", http.StatusOK, response.Code) } body := response.Body.String() for _, want := range []string{ `data-image-browser`, `data-active-index="1"`, `href="/day/20260101/images?idx=0"`, `href="/day/20260101/images?idx=1"`, `src="/raw/image/20260101/images/A26010112050001.jpg"`, `src="/thumb/image/20260101/images/A26010112000001.jpg"`, `data-prev-index="0"`, `data-next-index="0"`, `keydown`, `ArrowLeft`, `ArrowRight`, `ring-2 ring-indigo-400`, } { if !strings.Contains(body, want) { t.Fatalf("expected response to contain %q\nbody:\n%s", want, body) } } } func TestVideoPageRendersAuthenticatedBrowser(t *testing.T) { router, cookie := newAuthenticatedRouter(t) request := httptest.NewRequest(http.MethodGet, "/day/20260101/videos?idx=1", nil) request.AddCookie(cookie) response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("expected status %d, got %d: %s", http.StatusOK, response.Code, response.Body.String()) } body := response.Body.String() for _, want := range []string{ `data-video-browser`, `data-active-index="1"`, `href="/day/20260101/videos?idx=0"`, `href="/day/20260101/videos?idx=1"`, `src="/stream/video/20260101/record/A260101_120500_120530.265"`, `src="/thumb/video/20260101/record/A260101_120000_120015.265"`, `data-nav="prev"`, `data-nav="next"`, `keydown`, `ArrowLeft`, `ArrowRight`, `ring-2 ring-indigo-400`, } { if !strings.Contains(body, want) { t.Fatalf("expected response to contain %q\nbody:\n%s", want, body) } } } func newAuthenticatedRouter(t *testing.T) (http.Handler, *http.Cookie) { t.Helper() database, err := db.Open(filepath.Join(t.TempDir(), "cammonitor.db")) if err != nil { t.Fatalf("open database: %v", err) } t.Cleanup(func() { if err := database.Close(); err != nil { t.Fatalf("close database: %v", err) } }) store := auth.NewStore(database) if err := store.EnsureAdmin("admin", "secret"); err != nil { t.Fatalf("ensure admin: %v", err) } footageRoot := filepath.Join("..", "..", "testdata", "footage") index := footage.NewIndex(footageRoot, time.Hour) t.Cleanup(index.Close) router := NewRouter(&config.Config{FootageRoot: footageRoot, SessionTTL: time.Hour}, database, index) cookie := login(t, router) return router, cookie } func login(t *testing.T, router http.Handler) *http.Cookie { t.Helper() form := url.Values{ "username": {"admin"}, "password": {"secret"}, } request := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode())) request.Header.Set("Content-Type", "application/x-www-form-urlencoded") response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusSeeOther { t.Fatalf("expected login status %d, got %d", http.StatusSeeOther, response.Code) } for _, cookie := range response.Result().Cookies() { if cookie.Name == "session" && cookie.Value != "" { return cookie } } t.Fatalf("expected login response to set session cookie") return nil }