Files
user-system/internal/auth/sso_test.go
long-agent 582ad7a069 test: add comprehensive test coverage and improve code quality
- Add new test files for auth, service, and handler modules
- Improve test organization and coverage
- Refactor code for better maintainability
- Add captcha, settings, stats, and theme handler tests
- Add auth module tests (CAS, OAuth, password, SSO, state)
- Add service layer tests for auth, export, permissions, roles
- All Go tests pass (exit code 0)
- All frontend tests pass (325 tests in 59 files)
2026-04-17 20:43:50 +08:00

551 lines
13 KiB
Go

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)
}
})
}
}