package web import ( "encoding/json" "io/fs" "log/slog" "net/http" "net/http/httptest" "testing" "testing/fstest" "git.wntrmute.dev/mc/mcdsl/auth" ) func TestSetSessionCookie(t *testing.T) { rec := httptest.NewRecorder() SetSessionCookie(rec, "my_token", "abc123") cookies := rec.Result().Cookies() var found *http.Cookie for _, c := range cookies { if c.Name == "my_token" { found = c break } } if found == nil { t.Fatal("cookie not set") } if found.Value != "abc123" { t.Fatalf("value = %q, want %q", found.Value, "abc123") } if !found.HttpOnly { t.Fatal("not HttpOnly") } if !found.Secure { t.Fatal("not Secure") } if found.SameSite != http.SameSiteStrictMode { t.Fatal("not SameSite=Strict") } } func TestClearSessionCookie(t *testing.T) { rec := httptest.NewRecorder() ClearSessionCookie(rec, "my_token") cookies := rec.Result().Cookies() var found *http.Cookie for _, c := range cookies { if c.Name == "my_token" { found = c break } } if found == nil { t.Fatal("cookie not set") } if found.MaxAge != -1 { t.Fatalf("MaxAge = %d, want -1", found.MaxAge) } if found.Value != "" { t.Fatalf("Value = %q, want empty", found.Value) } } func TestGetSessionToken(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) req.AddCookie(&http.Cookie{Name: "tok", Value: "mytoken"}) got := GetSessionToken(req, "tok") if got != "mytoken" { t.Fatalf("got %q, want %q", got, "mytoken") } } func TestGetSessionTokenMissing(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) got := GetSessionToken(req, "tok") if got != "" { t.Fatalf("got %q, want empty", got) } } // mockMCIAS returns a test HTTP server for auth testing. func mockMCIAS(t *testing.T) *httptest.Server { t.Helper() mux := http.NewServeMux() mux.HandleFunc("POST /v1/token/validate", func(w http.ResponseWriter, r *http.Request) { var req struct { Token string `json:"token"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "bad", http.StatusBadRequest) return } if req.Token == "valid-token" { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]interface{}{ "valid": true, "username": "testuser", "roles": []string{"user"}, }) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]interface{}{"valid": false}) }) return httptest.NewServer(mux) } func newTestAuth(t *testing.T, serverURL string) *auth.Authenticator { t.Helper() a, err := auth.New(auth.Config{ServerURL: serverURL}, slog.Default()) if err != nil { t.Fatalf("auth.New: %v", err) } return a } func TestRequireAuthRedirectsWhenMissing(t *testing.T) { srv := mockMCIAS(t) defer srv.Close() a := newTestAuth(t, srv.URL) handler := RequireAuth(a, "session", "/login")( http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { t.Fatal("handler should not be called") }), ) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/protected", nil) handler.ServeHTTP(rec, req) if rec.Code != http.StatusFound { t.Fatalf("status = %d, want %d", rec.Code, http.StatusFound) } if loc := rec.Header().Get("Location"); loc != "/login" { t.Fatalf("Location = %q, want %q", loc, "/login") } } func TestRequireAuthRedirectsWhenInvalid(t *testing.T) { srv := mockMCIAS(t) defer srv.Close() a := newTestAuth(t, srv.URL) handler := RequireAuth(a, "session", "/login")( http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { t.Fatal("handler should not be called") }), ) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/protected", nil) req.AddCookie(&http.Cookie{Name: "session", Value: "bad-token"}) handler.ServeHTTP(rec, req) if rec.Code != http.StatusFound { t.Fatalf("status = %d, want %d", rec.Code, http.StatusFound) } } func TestRequireAuthPassesValid(t *testing.T) { srv := mockMCIAS(t) defer srv.Close() a := newTestAuth(t, srv.URL) var gotInfo *auth.TokenInfo handler := RequireAuth(a, "session", "/login")( http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { gotInfo = auth.TokenInfoFromContext(r.Context()) }), ) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/protected", nil) req.AddCookie(&http.Cookie{Name: "session", Value: "valid-token"}) handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) } if gotInfo == nil { t.Fatal("TokenInfo not in context") } if gotInfo.Username != "testuser" { t.Fatalf("Username = %q, want %q", gotInfo.Username, "testuser") } } func TestRenderTemplate(t *testing.T) { testFS := fstest.MapFS{ "templates/layout.html": &fstest.MapFile{ Data: []byte(`{{define "layout"}}{{template "content" .}}{{end}}`), }, "templates/page.html": &fstest.MapFile{ Data: []byte(`{{define "content"}}

{{.Title}}

{{end}}`), }, } rec := httptest.NewRecorder() RenderTemplate(rec, fs.FS(testFS), "page.html", map[string]string{"Title": "Hello"}) body := rec.Body.String() if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) } if ct := rec.Header().Get("Content-Type"); ct != "text/html; charset=utf-8" { t.Fatalf("Content-Type = %q", ct) } if body != "

Hello

" { t.Fatalf("body = %q", body) } } func TestRenderTemplateWithFuncMap(t *testing.T) { testFS := fstest.MapFS{ "templates/layout.html": &fstest.MapFile{ Data: []byte(`{{define "layout"}}{{upper .Name}}{{end}}`), }, "templates/page.html": &fstest.MapFile{ Data: []byte(`{{define "content"}}unused{{end}}`), }, } rec := httptest.NewRecorder() RenderTemplate(rec, fs.FS(testFS), "page.html", map[string]string{"Name": "hello"}, map[string]any{ "upper": func(s string) string { return "HELLO" }, }, ) if rec.Body.String() != "HELLO" { t.Fatalf("body = %q, want %q", rec.Body.String(), "HELLO") } }