package handlers import ( "fmt" "net/http" "net/http/httptest" "strings" "testing" ) func TestNewSessionHandler(t *testing.T) { sessionManager := &MockSessionManager{} logger := &MockLogger{} logoutURLPath := "/logout" postLogoutRedirectURI := "https://example.com/post-logout" endSessionURL := "https://provider.example.com/logout" clientID := "test-client-id" handler := NewSessionHandler( sessionManager, logger, logoutURLPath, postLogoutRedirectURI, endSessionURL, clientID, ) if handler == nil { t.Fatal("NewSessionHandler returned nil") } if handler.sessionManager != sessionManager { t.Error("SessionManager not set correctly") } if handler.logger != logger { t.Error("Logger not set correctly") } if handler.logoutURLPath != logoutURLPath { t.Error("LogoutURLPath not set correctly") } if handler.postLogoutRedirectURI != postLogoutRedirectURI { t.Error("PostLogoutRedirectURI not set correctly") } if handler.endSessionURL != endSessionURL { t.Error("EndSessionURL not set correctly") } if handler.clientID != clientID { t.Error("ClientID not set correctly") } } func TestSessionHandler_HandleLogout(t *testing.T) { tests := []struct { name string setupSession func() *MockSession setupManager func() *MockSessionManager expectedCode int expectedURL string }{ { name: "Successful logout with ID token", setupSession: func() *MockSession { return &MockSession{ authenticated: true, idToken: "test-id-token", } }, setupManager: func() *MockSessionManager { return &MockSessionManager{ session: &MockSession{ authenticated: true, idToken: "test-id-token", }, } }, expectedCode: http.StatusFound, expectedURL: "https://provider.example.com/logout?id_token_hint=test-id-token&post_logout_redirect_uri=https://example.com/post-logout&client_id=test-client-id", }, { name: "Logout without ID token", setupSession: func() *MockSession { return &MockSession{ authenticated: true, idToken: "", } }, setupManager: func() *MockSessionManager { return &MockSessionManager{ session: &MockSession{ authenticated: true, idToken: "", }, } }, expectedCode: http.StatusFound, expectedURL: "https://provider.example.com/logout?post_logout_redirect_uri=https://example.com/post-logout&client_id=test-client-id", }, { name: "Session retrieval error", setupSession: func() *MockSession { return nil }, setupManager: func() *MockSessionManager { return &MockSessionManager{ err: fmt.Errorf("session error"), } }, expectedCode: http.StatusFound, expectedURL: "https://provider.example.com/logout?post_logout_redirect_uri=https://example.com/post-logout&client_id=test-client-id", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { handler := &SessionHandler{ sessionManager: test.setupManager(), logger: &MockLogger{}, logoutURLPath: "/logout", postLogoutRedirectURI: "https://example.com/post-logout", endSessionURL: "https://provider.example.com/logout", clientID: "test-client-id", } req := httptest.NewRequest("POST", "/logout", nil) rw := httptest.NewRecorder() handler.HandleLogout(rw, req) if rw.Code != test.expectedCode { t.Errorf("Expected status code %d, got %d", test.expectedCode, rw.Code) } location := rw.Header().Get("Location") if location != test.expectedURL { t.Errorf("Expected location '%s', got '%s'", test.expectedURL, location) } }) } } func TestSessionHandler_buildLogoutURL(t *testing.T) { tests := []struct { name string endSessionURL string postLogoutRedirectURI string clientID string idToken string expected string }{ { name: "Complete logout URL with all parameters", endSessionURL: "https://provider.example.com/logout", postLogoutRedirectURI: "https://example.com/post-logout", clientID: "test-client-id", idToken: "test-id-token", expected: "https://provider.example.com/logout?id_token_hint=test-id-token&post_logout_redirect_uri=https://example.com/post-logout&client_id=test-client-id", }, { name: "Logout URL without ID token", endSessionURL: "https://provider.example.com/logout", postLogoutRedirectURI: "https://example.com/post-logout", clientID: "test-client-id", idToken: "", expected: "https://provider.example.com/logout?post_logout_redirect_uri=https://example.com/post-logout&client_id=test-client-id", }, { name: "No end session URL", endSessionURL: "", postLogoutRedirectURI: "https://example.com/post-logout", clientID: "test-client-id", idToken: "test-id-token", expected: "https://example.com/post-logout", }, { name: "End session URL with existing query parameters", endSessionURL: "https://provider.example.com/logout?foo=bar", postLogoutRedirectURI: "https://example.com/post-logout", clientID: "test-client-id", idToken: "", expected: "https://provider.example.com/logout?foo=bar&post_logout_redirect_uri=https://example.com/post-logout&client_id=test-client-id", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { handler := &SessionHandler{ endSessionURL: test.endSessionURL, postLogoutRedirectURI: test.postLogoutRedirectURI, clientID: test.clientID, } result := handler.buildLogoutURL(test.idToken) if result != test.expected { t.Errorf("Expected '%s', got '%s'", test.expected, result) } }) } } func TestSessionHandler_ValidateSession(t *testing.T) { handler := &SessionHandler{} tests := []struct { name string session Session expectedValid bool expectedAuth bool expectedMessage string }{ { name: "Nil session", session: nil, expectedValid: false, expectedAuth: true, expectedMessage: "session is nil", }, { name: "Not authenticated session", session: &MockSession{ authenticated: false, }, expectedValid: false, expectedAuth: true, expectedMessage: "session not authenticated", }, { name: "Authenticated session without email", session: &MockSession{ authenticated: true, email: "", }, expectedValid: false, expectedAuth: true, expectedMessage: "no email in session", }, { name: "Valid authenticated session with email", session: &MockSession{ authenticated: true, email: "user@example.com", }, expectedValid: true, expectedAuth: false, expectedMessage: "", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { result := handler.ValidateSession(test.session) if result.Valid != test.expectedValid { t.Errorf("Expected Valid %v, got %v", test.expectedValid, result.Valid) } if result.NeedsAuth != test.expectedAuth { t.Errorf("Expected NeedsAuth %v, got %v", test.expectedAuth, result.NeedsAuth) } if result.ErrorMessage != test.expectedMessage { t.Errorf("Expected ErrorMessage '%s', got '%s'", test.expectedMessage, result.ErrorMessage) } }) } } func TestSessionHandler_CleanupExpiredSession(t *testing.T) { tests := []struct { name string session *MockSession expectError bool }{ { name: "Successful cleanup", session: &MockSession{ authenticated: true, email: "user@example.com", refreshToken: "refresh-token", }, expectError: false, }, { name: "Save error during cleanup", session: &MockSession{ authenticated: true, email: "user@example.com", refreshToken: "refresh-token", saveError: fmt.Errorf("save failed"), }, expectError: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { handler := &SessionHandler{ logger: &MockLogger{}, } req := httptest.NewRequest("GET", "/", nil) rw := httptest.NewRecorder() err := handler.CleanupExpiredSession(rw, req, test.session) if test.expectError && err == nil { t.Error("Expected error but got nil") } if !test.expectError && err != nil { t.Errorf("Expected no error but got: %v", err) } if test.session != nil && !test.expectError { if test.session.authenticated { t.Error("Expected session authenticated to be false after cleanup") } if test.session.email != "" { t.Error("Expected session email to be empty after cleanup") } if test.session.refreshToken != "" { t.Error("Expected session refresh token to be empty after cleanup") } } }) } // Test nil session separately t.Run("Nil session", func(t *testing.T) { handler := &SessionHandler{ logger: &MockLogger{}, } req := httptest.NewRequest("GET", "/", nil) rw := httptest.NewRecorder() var nilSession Session = nil err := handler.CleanupExpiredSession(rw, req, nilSession) if err != nil { t.Errorf("Expected no error for nil session, got: %v", err) } }) } func TestSessionHandler_IsAjaxRequest(t *testing.T) { handler := &SessionHandler{} tests := []struct { name string headers map[string]string expected bool }{ { name: "XMLHttpRequest header", headers: map[string]string{ "X-Requested-With": "XMLHttpRequest", }, expected: true, }, { name: "JSON Accept header without HTML", headers: map[string]string{ "Accept": "application/json", }, expected: true, }, { name: "JSON Accept header with HTML", headers: map[string]string{ "Accept": "application/json, text/html", }, expected: false, }, { name: "Fetch API CORS mode", headers: map[string]string{ "Sec-Fetch-Mode": "cors", }, expected: true, }, { name: "Regular browser request", headers: map[string]string{ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", }, expected: false, }, { name: "No special headers", headers: map[string]string{}, expected: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { req := httptest.NewRequest("GET", "/", nil) for key, value := range test.headers { req.Header.Set(key, value) } result := handler.IsAjaxRequest(req) if result != test.expected { t.Errorf("Expected %v, got %v", test.expected, result) } }) } } func TestSessionHandler_SendErrorResponse(t *testing.T) { tests := []struct { name string isAjax bool message string statusCode int expectedContentType string expectedBodyContains string }{ { name: "AJAX error response", isAjax: true, message: "Authentication failed", statusCode: http.StatusUnauthorized, expectedContentType: "application/json", expectedBodyContains: `{"error": "Authentication failed"}`, }, { name: "Browser error response", isAjax: false, message: "Session expired", statusCode: http.StatusForbidden, expectedContentType: "text/html", expectedBodyContains: "