package auth import ( "context" "testing" "time" ) func TestNewSSOManager(t *testing.T) { m := NewSSOManager() if m == nil { t.Fatal("NewSSOManager() returned nil") } if m.sessions == nil { t.Error("NewSSOManager() did not initialize sessions map") } } func TestGenerateSecureToken(t *testing.T) { token, err := generateSecureToken(32) if err != nil { t.Fatalf("generateSecureToken() error = %v", err) } if len(token) != 32 { t.Errorf("generateSecureToken() length = %d, want 32", len(token)) } // Generate another token and verify they're different token2, err := generateSecureToken(32) if err != nil { t.Fatalf("generateSecureToken() error = %v", err) } if token == token2 { t.Error("generateSecureToken() generated identical tokens") } } func TestSSOManager_GenerateAuthorizationCode(t *testing.T) { m := NewSSOManager() code, err := m.GenerateAuthorizationCode("client-1", "https://example.com/callback", "openid", 123, "testuser") if err != nil { t.Fatalf("GenerateAuthorizationCode() error = %v", err) } if code == "" { t.Error("GenerateAuthorizationCode() returned empty code") } // Verify session was stored m.mu.RLock() _, exists := m.sessions[code] m.mu.RUnlock() if !exists { t.Error("GenerateAuthorizationCode() did not store session") } } func TestSSOManager_ValidateAuthorizationCode(t *testing.T) { m := NewSSOManager() // Generate a code first code, _ := m.GenerateAuthorizationCode("client-1", "https://example.com/callback", "openid", 123, "testuser") session, err := m.ValidateAuthorizationCode(code) if err != nil { t.Fatalf("ValidateAuthorizationCode() error = %v", err) } if session.UserID != 123 { t.Errorf("UserID = %d, want 123", session.UserID) } if session.Username != "testuser" { t.Errorf("Username = %s, want testuser", session.Username) } if session.ClientID != "client-1" { t.Errorf("ClientID = %s, want client-1", session.ClientID) } // Code should be consumed (one-time use) _, err = m.ValidateAuthorizationCode(code) if err == nil { t.Error("ValidateAuthorizationCode() should return error for consumed code") } } func TestSSOManager_ValidateAuthorizationCode_Invalid(t *testing.T) { m := NewSSOManager() _, err := m.ValidateAuthorizationCode("invalid-code") if err == nil { t.Error("ValidateAuthorizationCode() should return error for invalid code") } } func TestSSOManager_ValidateAuthorizationCode_Expired(t *testing.T) { m := NewSSOManager() // Generate a code code, _ := m.GenerateAuthorizationCode("client-1", "https://example.com/callback", "openid", 123, "testuser") // Manually expire it m.mu.Lock() session := m.sessions[code] session.ExpiresAt = time.Now().Add(-1 * time.Hour) m.mu.Unlock() _, err := m.ValidateAuthorizationCode(code) if err == nil { t.Error("ValidateAuthorizationCode() should return error for expired code") } } func TestSSOManager_GenerateAccessToken(t *testing.T) { m := NewSSOManager() session := &SSOSession{ UserID: 123, Username: "testuser", Scope: "openid", } token, expiresAt, err := m.GenerateAccessToken("client-1", session) if err != nil { t.Fatalf("GenerateAccessToken() error = %v", err) } if token == "" { t.Error("GenerateAccessToken() returned empty token") } if expiresAt.Before(time.Now()) { t.Error("GenerateAccessToken() returned expired time") } // Verify token was stored m.mu.RLock() storedSession, exists := m.sessions[token] m.mu.RUnlock() if !exists { t.Error("GenerateAccessToken() did not store session") } if storedSession.UserID != 123 { t.Errorf("Stored UserID = %d, want 123", storedSession.UserID) } } func TestSSOManager_IntrospectToken(t *testing.T) { m := NewSSOManager() session := &SSOSession{ UserID: 123, Username: "testuser", Scope: "openid", } token, _, _ := m.GenerateAccessToken("client-1", session) info, err := m.IntrospectToken(token) if err != nil { t.Fatalf("IntrospectToken() error = %v", err) } if !info.Active { t.Error("IntrospectToken() returned inactive for valid token") } if info.UserID != 123 { t.Errorf("UserID = %d, want 123", info.UserID) } if info.Username != "testuser" { t.Errorf("Username = %s, want testuser", info.Username) } } func TestSSOManager_IntrospectToken_Invalid(t *testing.T) { m := NewSSOManager() info, err := m.IntrospectToken("invalid-token") if err != nil { t.Fatalf("IntrospectToken() error = %v", err) } if info.Active { t.Error("IntrospectToken() should return inactive for invalid token") } } func TestSSOManager_IntrospectToken_Expired(t *testing.T) { m := NewSSOManager() session := &SSOSession{ UserID: 123, Username: "testuser", Scope: "openid", } token, _, _ := m.GenerateAccessToken("client-1", session) // Manually expire it m.mu.Lock() m.sessions[token].ExpiresAt = time.Now().Add(-1 * time.Hour) m.mu.Unlock() info, err := m.IntrospectToken(token) if err != nil { t.Fatalf("IntrospectToken() error = %v", err) } if info.Active { t.Error("IntrospectToken() should return inactive for expired token") } } func TestSSOManager_RevokeToken(t *testing.T) { m := NewSSOManager() session := &SSOSession{ UserID: 123, Username: "testuser", Scope: "openid", } token, _, _ := m.GenerateAccessToken("client-1", session) err := m.RevokeToken(token) if err != nil { t.Fatalf("RevokeToken() error = %v", err) } // Token should be removed m.mu.RLock() _, exists := m.sessions[token] m.mu.RUnlock() if exists { t.Error("RevokeToken() did not remove token") } } func TestSSOManager_CleanupExpired(t *testing.T) { m := NewSSOManager() // Add sessions session1 := &SSOSession{ UserID: 123, Username: "user1", Scope: "openid", CreatedAt: time.Now(), ExpiresAt: time.Now().Add(1 * time.Hour), // Valid } session2 := &SSOSession{ UserID: 456, Username: "user2", Scope: "openid", CreatedAt: time.Now(), ExpiresAt: time.Now().Add(-1 * time.Hour), // Expired } m.mu.Lock() m.sessions["valid-token"] = session1 m.sessions["expired-token"] = session2 m.mu.Unlock() m.CleanupExpired() m.mu.RLock() defer m.mu.RUnlock() // Valid session should remain if _, exists := m.sessions["valid-token"]; !exists { t.Error("CleanupExpired() removed valid session") } // Expired session should be removed if _, exists := m.sessions["expired-token"]; exists { t.Error("CleanupExpired() did not remove expired session") } } func TestSSOManager_evictOldest(t *testing.T) { m := NewSSOManager() // Add sessions with different creation times oldSession := &SSOSession{ UserID: 123, Username: "old-user", Scope: "openid", CreatedAt: time.Now().Add(-1 * time.Hour), ExpiresAt: time.Now().Add(1 * time.Hour), } newSession := &SSOSession{ UserID: 456, Username: "new-user", Scope: "openid", CreatedAt: time.Now(), ExpiresAt: time.Now().Add(1 * time.Hour), } m.mu.Lock() m.sessions["old-token"] = oldSession m.sessions["new-token"] = newSession m.mu.Unlock() m.mu.Lock() m.evictOldest() m.mu.Unlock() // Oldest session should be removed m.mu.RLock() defer m.mu.RUnlock() if _, exists := m.sessions["old-token"]; exists { t.Error("evictOldest() did not remove oldest session") } if _, exists := m.sessions["new-token"]; !exists { t.Error("evictOldest() removed newer session") } } func TestSSOManager_evictOldest_Empty(t *testing.T) { m := NewSSOManager() // Should not panic with empty sessions m.mu.Lock() m.evictOldest() m.mu.Unlock() } func TestSSOManager_SessionCount(t *testing.T) { m := NewSSOManager() if m.SessionCount() != 0 { t.Errorf("SessionCount() = %d, want 0", m.SessionCount()) } m.mu.Lock() m.sessions["token1"] = &SSOSession{UserID: 1} m.sessions["token2"] = &SSOSession{UserID: 2} m.mu.Unlock() if m.SessionCount() != 2 { t.Errorf("SessionCount() = %d, want 2", m.SessionCount()) } } func TestSSOManager_StartCleanup(t *testing.T) { m := NewSSOManager() ctx, cancel := context.WithCancel(context.Background()) defer cancel() m.StartCleanup(ctx) // Add an expired session m.mu.Lock() m.sessions["expired"] = &SSOSession{ UserID: 1, ExpiresAt: time.Now().Add(-1 * time.Hour), } m.mu.Unlock() // Let cleanup run time.Sleep(100 * time.Millisecond) // Cancel context to stop cleanup cancel() time.Sleep(100 * time.Millisecond) } func TestSSOManager_MaxSessions(t *testing.T) { m := NewSSOManager() // Fill up sessions to max for i := 0; i < MaxSessions; i++ { token, _ := generateSecureToken(32) m.mu.Lock() m.sessions[token] = &SSOSession{ UserID: int64(i), CreatedAt: time.Now().Add(-time.Duration(i) * time.Second), ExpiresAt: time.Now().Add(1 * time.Hour), } m.mu.Unlock() } // Generate one more - should trigger eviction code, err := m.GenerateAuthorizationCode("client-1", "https://example.com/callback", "openid", 99999, "newuser") if err != nil { t.Fatalf("GenerateAuthorizationCode() error = %v", err) } // New session should exist m.mu.RLock() _, exists := m.sessions[code] m.mu.RUnlock() if !exists { t.Error("GenerateAuthorizationCode() did not store session at max capacity") } } func TestSSOManager_GenerateAccessToken_MaxSessions(t *testing.T) { m := NewSSOManager() // Fill up sessions to max for i := 0; i < MaxSessions; i++ { token, _ := generateSecureToken(32) m.mu.Lock() m.sessions[token] = &SSOSession{ UserID: int64(i), CreatedAt: time.Now().Add(-time.Duration(i) * time.Second), ExpiresAt: time.Now().Add(1 * time.Hour), } m.mu.Unlock() } // Generate access token - should trigger eviction session := &SSOSession{ UserID: 99999, Username: "newuser", Scope: "openid", } token, expiresAt, err := m.GenerateAccessToken("client-1", session) if err != nil { t.Fatalf("GenerateAccessToken() error = %v", err) } if token == "" { t.Error("GenerateAccessToken() returned empty token") } if expiresAt.Before(time.Now()) { t.Error("GenerateAccessToken() returned expired time") } // Verify token was stored m.mu.RLock() _, exists := m.sessions[token] m.mu.RUnlock() if !exists { t.Error("GenerateAccessToken() did not store session at max capacity") } } func TestSSOManager_GenerateAccessToken_WithExpiredSessions(t *testing.T) { m := NewSSOManager() // Add some expired sessions for i := 0; i < 5; i++ { token, _ := generateSecureToken(32) m.mu.Lock() m.sessions[token] = &SSOSession{ UserID: int64(i), CreatedAt: time.Now().Add(-2 * time.Hour), ExpiresAt: time.Now().Add(-1 * time.Hour), // Expired } m.mu.Unlock() } // Generate access token - should clean up expired sessions first session := &SSOSession{ UserID: 123, Username: "testuser", Scope: "openid", } _, _, err := m.GenerateAccessToken("client-1", session) if err != nil { t.Fatalf("GenerateAccessToken() error = %v", err) } // Verify expired sessions were cleaned m.mu.RLock() count := len(m.sessions) m.mu.RUnlock() if count > MaxSessions { t.Errorf("Session count %d exceeds max %d", count, MaxSessions) } } // DefaultSSOClientsStore tests func TestNewDefaultSSOClientsStore(t *testing.T) { store := NewDefaultSSOClientsStore() if store == nil { t.Fatal("NewDefaultSSOClientsStore() returned nil") } if store.clients == nil { t.Error("NewDefaultSSOClientsStore() did not initialize clients map") } } func TestDefaultSSOClientsStore_RegisterClient(t *testing.T) { store := NewDefaultSSOClientsStore() client := &SSOClient{ ClientID: "client-1", ClientSecret: "secret", Name: "Test Client", RedirectURIs: []string{"https://example.com/callback"}, } store.RegisterClient(client) retrieved, err := store.GetByClientID("client-1") if err != nil { t.Fatalf("GetByClientID() error = %v", err) } if retrieved.Name != "Test Client" { t.Errorf("Name = %s, want Test Client", retrieved.Name) } } func TestDefaultSSOClientsStore_GetByClientID_NotFound(t *testing.T) { store := NewDefaultSSOClientsStore() _, err := store.GetByClientID("non-existent") if err == nil { t.Error("GetByClientID() should return error for non-existent client") } } func TestDefaultSSOClientsStore_ValidateClientRedirectURI(t *testing.T) { store := NewDefaultSSOClientsStore() client := &SSOClient{ ClientID: "client-1", ClientSecret: "secret", Name: "Test Client", RedirectURIs: []string{"https://example.com/callback", "https://app.com/auth"}, } store.RegisterClient(client) tests := []struct { name string clientID string redirectURI string want bool }{ {"valid URI", "client-1", "https://example.com/callback", true}, {"another valid URI", "client-1", "https://app.com/auth", true}, {"invalid URI", "client-1", "https://evil.com/callback", false}, {"invalid client", "unknown", "https://example.com/callback", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := store.ValidateClientRedirectURI(tt.clientID, tt.redirectURI) if result != tt.want { t.Errorf("ValidateClientRedirectURI() = %v, want %v", result, tt.want) } }) } }