package web import ( "context" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "git.t-juice.club/torjus/oubliette/internal/storage" ) func newTestServer(t *testing.T) *Server { t.Helper() store := storage.NewMemoryStore() logger := slog.Default() srv, err := NewServer(store, logger) if err != nil { t.Fatalf("creating server: %v", err) } return srv } func newSeededTestServer(t *testing.T) *Server { t.Helper() store := storage.NewMemoryStore() ctx := context.Background() for i := 0; i < 5; i++ { if err := store.RecordLoginAttempt(ctx, "root", "toor", "10.0.0.1"); err != nil { t.Fatalf("seeding attempt: %v", err) } } if err := store.RecordLoginAttempt(ctx, "admin", "admin", "10.0.0.2"); err != nil { t.Fatalf("seeding attempt: %v", err) } if _, err := store.CreateSession(ctx, "10.0.0.1", "root", "bash"); err != nil { t.Fatalf("creating session: %v", err) } if _, err := store.CreateSession(ctx, "10.0.0.2", "admin", "bash"); err != nil { t.Fatalf("creating session: %v", err) } logger := slog.Default() srv, err := NewServer(store, logger) if err != nil { t.Fatalf("creating server: %v", err) } return srv } func TestDashboardHandler(t *testing.T) { t.Run("empty store", func(t *testing.T) { srv := newTestServer(t) req := httptest.NewRequest("GET", "/", nil) w := httptest.NewRecorder() srv.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } body := w.Body.String() if !strings.Contains(body, "Oubliette") { t.Error("response should contain 'Oubliette'") } if !strings.Contains(body, "No data") { t.Error("response should contain 'No data' for empty tables") } }) t.Run("with data", func(t *testing.T) { srv := newSeededTestServer(t) req := httptest.NewRequest("GET", "/", nil) w := httptest.NewRecorder() srv.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } body := w.Body.String() if !strings.Contains(body, "root") { t.Error("response should contain username 'root'") } if !strings.Contains(body, "10.0.0.1") { t.Error("response should contain IP '10.0.0.1'") } }) } func TestFragmentStats(t *testing.T) { srv := newSeededTestServer(t) req := httptest.NewRequest("GET", "/fragments/stats", nil) w := httptest.NewRecorder() srv.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } body := w.Body.String() // Should be a fragment, not a full HTML page. if strings.Contains(body, "") { t.Error("stats fragment should not contain full HTML document") } if !strings.Contains(body, "Total Attempts") { t.Error("stats fragment should contain 'Total Attempts'") } } func TestFragmentActiveSessions(t *testing.T) { srv := newSeededTestServer(t) req := httptest.NewRequest("GET", "/fragments/active-sessions", nil) w := httptest.NewRecorder() srv.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } body := w.Body.String() if strings.Contains(body, "") { t.Error("active sessions fragment should not contain full HTML document") } // Both sessions are active (not ended). if !strings.Contains(body, "10.0.0.1") { t.Error("active sessions should contain IP '10.0.0.1'") } } func TestStaticAssets(t *testing.T) { srv := newTestServer(t) tests := []struct { path string contentType string }{ {"/static/pico.min.css", "text/css"}, {"/static/htmx.min.js", "text/javascript"}, } for _, tt := range tests { t.Run(tt.path, func(t *testing.T) { req := httptest.NewRequest("GET", tt.path, nil) w := httptest.NewRecorder() srv.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } ct := w.Header().Get("Content-Type") if !strings.Contains(ct, tt.contentType) { t.Errorf("Content-Type = %q, want to contain %q", ct, tt.contentType) } }) } }