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)
This commit is contained in:
550
internal/auth/sso_test.go
Normal file
550
internal/auth/sso_test.go
Normal file
@@ -0,0 +1,550 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user