fetch_ml/tests/unit/auth/user_manager_test.go
Jeremie Fraeys c980167041 test: implement comprehensive test suite with multiple test types
- Add end-to-end tests for complete workflow validation
- Include integration tests for API and database interactions
- Add unit tests for all major components and utilities
- Include performance tests for payload handling
- Add CLI API integration tests
- Include Podman container integration tests
- Add WebSocket and queue execution tests
- Include shell script tests for setup validation

Provides comprehensive test coverage ensuring platform reliability
and functionality across all components and interactions.
2025-12-04 16:55:13 -05:00

333 lines
7.9 KiB
Go

package auth
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/jfraeys/fetch_ml/internal/auth"
"gopkg.in/yaml.v3"
)
// ConfigWithAuth holds configuration with authentication
type ConfigWithAuth struct {
Auth auth.AuthConfig `yaml:"auth"`
}
func TestUserManagerGenerateKey(t *testing.T) {
// Create temporary config file
tempDir := t.TempDir()
configFile := filepath.Join(tempDir, "test_config.yaml")
// Initial config with auth enabled
config := ConfigWithAuth{
Auth: auth.AuthConfig{
Enabled: true,
APIKeys: map[auth.Username]auth.APIKeyEntry{
"existing_user": {
Hash: auth.APIKeyHash(auth.HashAPIKey("existing-key")),
Admin: false,
},
},
},
}
data, err := yaml.Marshal(config)
if err != nil {
t.Fatalf("Failed to marshal config: %v", err)
}
if err := os.WriteFile(configFile, data, 0644); err != nil {
t.Fatalf("Failed to write config: %v", err)
}
// Test generate-key command
configData, err := os.ReadFile(configFile)
if err != nil {
t.Fatalf("Failed to read config: %v", err)
}
var cfg ConfigWithAuth
if err := yaml.Unmarshal(configData, &cfg); err != nil {
t.Fatalf("Failed to parse config: %v", err)
}
// Generate API key
apiKey := auth.GenerateAPIKey()
// Add to config
if cfg.Auth.APIKeys == nil {
cfg.Auth.APIKeys = make(map[auth.Username]auth.APIKeyEntry)
}
cfg.Auth.APIKeys[auth.Username("test_user")] = auth.APIKeyEntry{
Hash: auth.APIKeyHash(auth.HashAPIKey(apiKey)),
Admin: false,
}
// Save config
updatedData, err := yaml.Marshal(cfg)
if err != nil {
t.Fatalf("Failed to marshal updated config: %v", err)
}
if err := os.WriteFile(configFile, updatedData, 0644); err != nil {
t.Fatalf("Failed to write updated config: %v", err)
}
// Verify user was added
savedData, err := os.ReadFile(configFile)
if err != nil {
t.Fatalf("Failed to read saved config: %v", err)
}
var savedCfg ConfigWithAuth
if err := yaml.Unmarshal(savedData, &savedCfg); err != nil {
t.Fatalf("Failed to parse saved config: %v", err)
}
if _, exists := savedCfg.Auth.APIKeys["test_user"]; !exists {
t.Error("test_user was not added to config")
}
// Verify existing user still exists
if _, exists := savedCfg.Auth.APIKeys["existing_user"]; !exists {
t.Error("existing_user was removed from config")
}
}
func TestUserManagerListUsers(t *testing.T) {
// Create temporary config file
tempDir := t.TempDir()
configFile := filepath.Join(tempDir, "test_config.yaml")
// Initial config
config := ConfigWithAuth{
Auth: auth.AuthConfig{
Enabled: true,
APIKeys: map[auth.Username]auth.APIKeyEntry{
"admin": {
Hash: auth.APIKeyHash(auth.HashAPIKey("admin-key")),
Admin: true,
},
"regular": {
Hash: auth.APIKeyHash(auth.HashAPIKey("user-key")),
Admin: false,
},
"admin_user": {
Hash: auth.APIKeyHash(auth.HashAPIKey("adminuser-key")),
Admin: true,
},
},
},
}
data, err := yaml.Marshal(config)
if err != nil {
t.Fatalf("Failed to marshal config: %v", err)
}
if err := os.WriteFile(configFile, data, 0644); err != nil {
t.Fatalf("Failed to write config: %v", err)
}
// Load and verify config
configData, err := os.ReadFile(configFile)
if err != nil {
t.Fatalf("Failed to read config: %v", err)
}
var cfg ConfigWithAuth
if err := yaml.Unmarshal(configData, &cfg); err != nil {
t.Fatalf("Failed to parse config: %v", err)
}
// Test user listing
userCount := len(cfg.Auth.APIKeys)
expectedCount := 3
if userCount != expectedCount {
t.Errorf("Expected %d users, got %d", expectedCount, userCount)
}
// Verify admin detection
keyMap := map[auth.Username]string{
"admin": "admin-key",
"regular": "user-key",
"admin_user": "adminuser-key",
}
for username := range cfg.Auth.APIKeys {
testKey := keyMap[username]
user, err := cfg.Auth.ValidateAPIKey(testKey)
if err != nil {
t.Errorf("Failed to validate user %s: %v", username, err)
continue // Skip admin check if validation failed
}
expectedAdmin := username == "admin" || username == "admin_user"
if user.Admin != expectedAdmin {
t.Errorf("User %s: expected admin=%v, got admin=%v", username, expectedAdmin, user.Admin)
}
}
}
func TestUserManagerHashKey(t *testing.T) {
key := "test-api-key-123"
expectedHash := auth.HashAPIKey(key)
if expectedHash == "" {
t.Error("Hash should not be empty")
}
if len(expectedHash) != 64 {
t.Errorf("Expected hash length 64, got %d", len(expectedHash))
}
// Test consistency
hash2 := auth.HashAPIKey(key)
if expectedHash != hash2 {
t.Error("Hash should be consistent")
}
}
func TestConfigPersistence(t *testing.T) {
// Create temporary config file
tempDir := t.TempDir()
configFile := filepath.Join(tempDir, "test_config.yaml")
// Create initial config
config := ConfigWithAuth{
Auth: auth.AuthConfig{
Enabled: true,
APIKeys: map[auth.Username]auth.APIKeyEntry{},
},
}
data, err := yaml.Marshal(config)
if err != nil {
t.Fatalf("Failed to marshal config: %v", err)
}
if err := os.WriteFile(configFile, data, 0644); err != nil {
t.Fatalf("Failed to write config: %v", err)
}
// Simulate multiple operations
operations := []struct {
username string
apiKey string
}{
{"user1", "key1"},
{"user2", "key2"},
{"admin_user", "admin-key"},
}
for _, op := range operations {
// Load config
configData, err := os.ReadFile(configFile)
if err != nil {
t.Fatalf("Failed to read config: %v", err)
}
var cfg ConfigWithAuth
if err := yaml.Unmarshal(configData, &cfg); err != nil {
t.Fatalf("Failed to parse config: %v", err)
}
// Add user
if cfg.Auth.APIKeys == nil {
cfg.Auth.APIKeys = make(map[auth.Username]auth.APIKeyEntry)
}
cfg.Auth.APIKeys[auth.Username(op.username)] = auth.APIKeyEntry{
Hash: auth.APIKeyHash(auth.HashAPIKey(op.apiKey)),
Admin: strings.Contains(strings.ToLower(op.username), "admin"),
}
// Save config
updatedData, err := yaml.Marshal(cfg)
if err != nil {
t.Fatalf("Failed to marshal updated config: %v", err)
}
if err := os.WriteFile(configFile, updatedData, 0644); err != nil {
t.Fatalf("Failed to write updated config: %v", err)
}
// Small delay to ensure file system consistency
time.Sleep(1 * time.Millisecond)
}
// Verify final state
finalData, err := os.ReadFile(configFile)
if err != nil {
t.Fatalf("Failed to read final config: %v", err)
}
var finalCfg ConfigWithAuth
if err := yaml.Unmarshal(finalData, &finalCfg); err != nil {
t.Fatalf("Failed to parse final config: %v", err)
}
if len(finalCfg.Auth.APIKeys) != len(operations) {
t.Errorf("Expected %d users, got %d", len(operations), len(finalCfg.Auth.APIKeys))
}
for _, op := range operations {
if _, exists := finalCfg.Auth.APIKeys[auth.Username(op.username)]; !exists {
t.Errorf("User %s not found in final config", op.username)
}
}
}
func TestAuthDisabled(t *testing.T) {
t.Setenv("FETCH_ML_ALLOW_INSECURE_AUTH", "1")
defer t.Setenv("FETCH_ML_ALLOW_INSECURE_AUTH", "")
// Create temporary config file with auth disabled
tempDir := t.TempDir()
configFile := filepath.Join(tempDir, "test_config.yaml")
config := ConfigWithAuth{
Auth: auth.AuthConfig{
Enabled: false,
APIKeys: map[auth.Username]auth.APIKeyEntry{}, // Empty
},
}
data, err := yaml.Marshal(config)
if err != nil {
t.Fatalf("Failed to marshal config: %v", err)
}
if err := os.WriteFile(configFile, data, 0644); err != nil {
t.Fatalf("Failed to write config: %v", err)
}
// Load config
configData, err := os.ReadFile(configFile)
if err != nil {
t.Fatalf("Failed to read config: %v", err)
}
var cfg ConfigWithAuth
if err := yaml.Unmarshal(configData, &cfg); err != nil {
t.Fatalf("Failed to parse config: %v", err)
}
// Test validation with auth disabled
user, err := cfg.Auth.ValidateAPIKey("any-key")
if err != nil {
t.Errorf("Unexpected error with auth disabled: %v", err)
}
if user.Name != "default" {
t.Errorf("Expected default user, got %s", user.Name)
}
if !user.Admin {
t.Error("Default user should be admin")
}
}