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.Config `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.Config{ 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, 0600); err != nil { t.Fatalf("Failed to write config: %v", err) } // Test generate-key command configData, err := os.ReadFile(filepath.Clean(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 using new Argon2id method _, hashed, err := auth.GenerateAPIKey() if err != nil { t.Fatalf("Failed to generate API key: %v", err) } // Add to config with algorithm info 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(hashed.Hash), Salt: hashed.Salt, Algorithm: string(hashed.Algorithm), 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, 0600); 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.Config{ 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, 0600); err != nil { t.Fatalf("Failed to write config: %v", err) } // Load and verify config configData, err := os.ReadFile(filepath.Clean(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.Config{ 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, 0600); 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(filepath.Clean(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, 0600); 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.Config{ 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, 0600); err != nil { t.Fatalf("Failed to write config: %v", err) } // Load config configData, err := os.ReadFile(filepath.Clean(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") } }