package kate import ( "net/http" "net/http/httptest" "net/url" "strings" "testing" ) // Mock implementation of PasswordUserDataStore for testing type mockPasswordUserDataStore[T any] struct { users map[string]T getHash func(T) string } func newMockPasswordUserDataStore[T any](users map[string]T, getHash func(T) string) *mockPasswordUserDataStore[T] { return &mockPasswordUserDataStore[T]{ users: users, getHash: getHash, } } func (m *mockPasswordUserDataStore[T]) Fetch(username string) (T, bool, error) { user, exists := m.users[username] return user, exists, nil } func (m *mockPasswordUserDataStore[T]) GetPassHash(userData T) string { return m.getHash(userData) } func TestPasswordLoginHandler(t *testing.T) { auth := New("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", AuthConfig[testUser]{ SerDes: testUserSerDes{}, CookieName: "test_session", }) testHash, err := HashPassword("password123", nil) if err != nil { t.Fatalf("Failed to hash password: %v", err) } users := map[string]testUser{ "john": {Username: "john", Hash: testHash, ID: 1}, "jane": {Username: "jane", Hash: testHash, ID: 2}, } mockStore := newMockPasswordUserDataStore(users, func(user testUser) string { return user.Hash }) config := PasswordLoginConfig[testUser]{ UserData: mockStore, Redirects: Redirects{ Default: "/dashboard", AllowedPrefixes: []string{"/app/", "/admin/"}, FieldName: "redirect", }, } handler := auth.PasswordLoginHandler(config) tests := []struct { name string method string formData url.Values expectedStatus int expectedRedirect string checkCookie bool }{ { name: "successful login", method: "POST", formData: url.Values{"username": {"john"}, "password": {"password123"}}, expectedStatus: http.StatusSeeOther, expectedRedirect: "/dashboard", checkCookie: true, }, { name: "successful login with redirect", method: "POST", formData: url.Values{"username": {"john"}, "password": {"password123"}, "redirect": {"/app/settings"}}, expectedStatus: http.StatusSeeOther, expectedRedirect: "/app/settings", checkCookie: true, }, { name: "invalid redirect falls back to default", method: "POST", formData: url.Values{"username": {"john"}, "password": {"password123"}, "redirect": {"/evil/"}}, expectedStatus: http.StatusSeeOther, expectedRedirect: "/dashboard", checkCookie: true, }, { name: "wrong password", method: "POST", formData: url.Values{"username": {"john"}, "password": {"wrongpass"}}, expectedStatus: http.StatusUnauthorized, checkCookie: false, }, { name: "nonexistent user", method: "POST", formData: url.Values{"username": {"nobody"}, "password": {"password123"}}, expectedStatus: http.StatusUnauthorized, checkCookie: false, }, { name: "missing username", method: "POST", formData: url.Values{"password": {"password123"}}, expectedStatus: http.StatusBadRequest, checkCookie: false, }, { name: "missing password", method: "POST", formData: url.Values{"username": {"john"}}, expectedStatus: http.StatusBadRequest, checkCookie: false, }, { name: "GET method not allowed", method: "GET", formData: url.Values{}, expectedStatus: http.StatusMethodNotAllowed, checkCookie: false, }, { name: "PUT method not allowed", method: "PUT", formData: url.Values{}, expectedStatus: http.StatusMethodNotAllowed, checkCookie: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var req *http.Request if tt.method == "POST" { req = httptest.NewRequest(tt.method, "/login", strings.NewReader(tt.formData.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") } else { req = httptest.NewRequest(tt.method, "/login", nil) } rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != tt.expectedStatus { t.Errorf("expected status %d, got %d", tt.expectedStatus, rr.Code) } if tt.expectedRedirect != "" { location := rr.Header().Get("Location") if location != tt.expectedRedirect { t.Errorf("expected redirect to %s, got %s", tt.expectedRedirect, location) } } if tt.checkCookie { cookies := rr.Result().Cookies() found := false for _, cookie := range cookies { if cookie.Name == "test_session" && cookie.Value != "" { found = true break } } if !found { t.Error("expected authentication cookie to be set") } } }) } } func TestPasswordLoginHandlerCustomFields(t *testing.T) { auth := New("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", AuthConfig[testUser]{ SerDes: testUserSerDes{}, CookieName: "test_session", }) testHash, err := HashPassword("secret", nil) if err != nil { t.Fatalf("Failed to hash password: %v", err) } users := map[string]testUser{ "admin": {Username: "admin", Hash: testHash, ID: 99}, } mockStore := newMockPasswordUserDataStore(users, func(user testUser) string { return user.Hash }) config := PasswordLoginConfig[testUser]{ UserData: mockStore, UsernameField: "email", PasswordField: "pass", Redirects: Redirects{ FieldName: "next", Default: "/home", }, } handler := auth.PasswordLoginHandler(config) formData := url.Values{ "email": {"admin"}, "pass": {"secret"}, "next": {"/custom"}, } req := httptest.NewRequest("POST", "/login", strings.NewReader(formData.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusSeeOther { t.Errorf("expected status %d, got %d", http.StatusSeeOther, rr.Code) } location := rr.Header().Get("Location") if location != "/custom" { t.Errorf("expected redirect to /custom, got %s", location) } } func TestPasswordLoginHandlerParseFormError(t *testing.T) { auth := New("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", AuthConfig[testUser]{ SerDes: testUserSerDes{}, CookieName: "test_session", }) users := map[string]testUser{ "test": {Username: "test", Hash: "", ID: 1}, } mockStore := newMockPasswordUserDataStore(users, func(user testUser) string { return user.Hash }) config := PasswordLoginConfig[testUser]{ UserData: mockStore, } handler := auth.PasswordLoginHandler(config) req := httptest.NewRequest("POST", "/login", strings.NewReader("username=%")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected status %d, got %d", http.StatusBadRequest, rr.Code) } } func TestPasswordLoginHandlerGetPassHashError(t *testing.T) { auth := New("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", AuthConfig[testUser]{ SerDes: testUserSerDes{}, CookieName: "test_session", }) users := map[string]testUser{ "user": {Username: "user", Hash: "", ID: 1}, } mockStore := newMockPasswordUserDataStore(users, func(user testUser) string { return user.Hash // Empty hash will cause password comparison to fail }) config := PasswordLoginConfig[testUser]{ UserData: mockStore, } handler := auth.PasswordLoginHandler(config) formData := url.Values{"username": {"user"}, "password": {"pass"}} req := httptest.NewRequest("POST", "/login", strings.NewReader(formData.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusUnauthorized { t.Errorf("expected status %d, got %d", http.StatusUnauthorized, rr.Code) } } func TestPasswordLoginHandlerMalformedHash(t *testing.T) { auth := New("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", AuthConfig[testUser]{ SerDes: testUserSerDes{}, CookieName: "test_session", }) users := map[string]testUser{ "user": {Username: "user", Hash: "invalid-hash", ID: 1}, } mockStore := newMockPasswordUserDataStore(users, func(user testUser) string { return user.Hash }) config := PasswordLoginConfig[testUser]{ UserData: mockStore, } handler := auth.PasswordLoginHandler(config) formData := url.Values{"username": {"user"}, "password": {"pass"}} req := httptest.NewRequest("POST", "/login", strings.NewReader(formData.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusUnauthorized { t.Errorf("expected status %d, got %d", http.StatusUnauthorized, rr.Code) } } func TestRedirectsIsValid(t *testing.T) { tests := []struct { name string target string allowedPrefixes []string expected bool }{ { name: "no restrictions allows anything", target: "/anything", allowedPrefixes: []string{}, expected: true, }, { name: "valid prefix match", target: "/app/dashboard", allowedPrefixes: []string{"/app/", "/admin/"}, expected: true, }, { name: "invalid prefix", target: "/evil/", allowedPrefixes: []string{"/app/", "/admin/"}, expected: false, }, { name: "absolute URL rejected", target: "https://evil.com/", allowedPrefixes: []string{"/app/"}, expected: false, }, { name: "protocol relative URL rejected", target: "//evil.com/", allowedPrefixes: []string{"/app/"}, expected: false, }, { name: "invalid URL rejected", target: ":/invalid", allowedPrefixes: []string{"/app/"}, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { redirects := Redirects{AllowedPrefixes: tt.allowedPrefixes} result := redirects.isValid(tt.target) if result != tt.expected { t.Errorf("Redirects{AllowedPrefixes: %v}.isValid(%q) = %v, want %v", tt.allowedPrefixes, tt.target, result, tt.expected) } }) } }