Enable 15 additional linters (gosec, errorlint, gocritic, modernize, misspell, bodyclose, sqlclosecheck, nilerr, unconvert, durationcheck, sloglint, wastedassign, usestdlibvars) with sensible exclusion rules. Fix all findings: errors.Is for error comparisons, run() pattern in main to avoid exitAfterDefer, ReadHeaderTimeout for Slowloris protection, bounds check in escape sequence reader, WaitGroup.Go, slices.Contains, range-over-int loops, and http.MethodGet constants. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
162 lines
3.9 KiB
Go
162 lines
3.9 KiB
Go
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 range 5 {
|
|
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(http.MethodGet, "/", 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(http.MethodGet, "/", 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(http.MethodGet, "/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, "<!DOCTYPE html>") {
|
|
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(http.MethodGet, "/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, "<!DOCTYPE html>") {
|
|
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(http.MethodGet, 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)
|
|
}
|
|
})
|
|
}
|
|
}
|