package auth import ( "testing" "github.com/jfraeys/fetch_ml/internal/auth" ) func TestGenerateAPIKey(t *testing.T) { t.Parallel() // Enable parallel execution key1 := auth.GenerateAPIKey() if len(key1) != 64 { // 32 bytes = 64 hex chars t.Errorf("Expected key length 64, got %d", len(key1)) } // Test uniqueness key2 := auth.GenerateAPIKey() if key1 == key2 { t.Error("Generated keys should be unique") } } func TestUserHasPermission(t *testing.T) { t.Parallel() tests := []struct { name string user *auth.User permission string want bool }{ { name: "wildcard grants all", user: &auth.User{Permissions: map[string]bool{"*": true}}, want: true, }, { name: "direct permission match", user: &auth.User{Permissions: map[string]bool{"jobs:create": true}}, permission: "jobs:create", want: true, }, { name: "hierarchical permission match", user: &auth.User{Permissions: map[string]bool{"jobs": true}}, permission: "jobs:create", want: true, }, { name: "missing permission", user: &auth.User{Permissions: map[string]bool{"jobs:read": true}}, permission: "jobs:create", want: false, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if got := tt.user.HasPermission(tt.permission); got != tt.want { t.Fatalf("HasPermission(%q) = %v, want %v", tt.permission, got, tt.want) } }) } } func TestUserHasRole(t *testing.T) { t.Parallel() user := &auth.User{ Roles: []string{"admin", "data_scientist"}, } if !user.HasRole("admin") { t.Fatal("expected admin role to be present") } if user.HasRole("operator") { t.Fatal("did not expect operator role to be present") } } func TestHashAPIKey(t *testing.T) { t.Parallel() // Enable parallel execution key := "test-key-123" hash := auth.HashAPIKey(key) if len(hash) != 64 { // SHA256 = 64 hex chars t.Errorf("Expected hash length 64, got %d", len(hash)) } // Test consistency hash2 := auth.HashAPIKey(key) if hash != hash2 { t.Error("Hash should be consistent for same key") } // Test different keys produce different hashes hash3 := auth.HashAPIKey("different-key") if hash == hash3 { t.Error("Different keys should produce different hashes") } } func TestHashAPIKeyKnownValues(t *testing.T) { t.Parallel() tests := []struct { name string key string expected string }{ { name: "password hash", key: "password", expected: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8", }, { name: "test hash", key: "test", expected: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if got := auth.HashAPIKey(tt.key); got != tt.expected { t.Fatalf("HashAPIKey(%q) = %s, want %s", tt.key, got, tt.expected) } }) } } func TestHashAPIKeyConsistency(t *testing.T) { t.Parallel() key := "consistency-key" hash1 := auth.HashAPIKey(key) hash2 := auth.HashAPIKey(key) if hash1 != hash2 { t.Fatalf("HashAPIKey() not deterministic: %s vs %s", hash1, hash2) } if len(hash1) != 64 { t.Fatalf("HashAPIKey() length = %d, want 64", len(hash1)) } } func TestValidateAPIKey(t *testing.T) { t.Parallel() // Enable parallel execution config := auth.Config{ Enabled: true, APIKeys: map[auth.Username]auth.APIKeyEntry{ "admin": { Hash: auth.APIKeyHash(auth.HashAPIKey("admin-key")), Admin: true, }, "data_scientist": { Hash: auth.APIKeyHash(auth.HashAPIKey("ds-key")), Admin: false, }, }, } tests := []struct { name string apiKey string wantErr bool wantUser string wantAdmin bool }{ { name: "valid admin key", apiKey: "admin-key", wantErr: false, wantUser: "admin", wantAdmin: true, }, { name: "valid user key", apiKey: "ds-key", wantErr: false, wantUser: "data_scientist", wantAdmin: false, }, { name: "invalid key", apiKey: "wrong-key", wantErr: true, }, { name: "empty key", apiKey: "", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { user, err := config.ValidateAPIKey(tt.apiKey) if tt.wantErr { if err == nil { t.Error("Expected error but got none") } return } if err != nil { t.Errorf("Unexpected error: %v", err) return } if user.Name != tt.wantUser { t.Errorf("Expected user %s, got %s", tt.wantUser, user.Name) } if user.Admin != tt.wantAdmin { t.Errorf("Expected admin %v, got %v", tt.wantAdmin, user.Admin) } }) } } func TestValidateAPIKeyAuthDisabled(t *testing.T) { t.Setenv("FETCH_ML_ALLOW_INSECURE_AUTH", "1") defer t.Setenv("FETCH_ML_ALLOW_INSECURE_AUTH", "") config := auth.Config{ Enabled: false, APIKeys: map[auth.Username]auth.APIKeyEntry{}, // Empty } user, err := config.ValidateAPIKey("any-key") if err != nil { t.Errorf("Unexpected error when auth disabled: %v", err) } if user == nil { t.Fatal("Expected user, got nil") } if user.Name != "default" { t.Errorf("Expected default user, got %s", user.Name) } if !user.Admin { t.Error("Default user should be admin") } } func TestAdminDetection(t *testing.T) { t.Parallel() // Enable parallel execution config := auth.Config{ Enabled: true, APIKeys: map[auth.Username]auth.APIKeyEntry{ "admin": {Hash: auth.APIKeyHash(auth.HashAPIKey("key1")), Admin: true}, "admin_user": {Hash: auth.APIKeyHash(auth.HashAPIKey("key2")), Admin: true}, "superadmin": {Hash: auth.APIKeyHash(auth.HashAPIKey("key3")), Admin: true}, "regular": {Hash: auth.APIKeyHash(auth.HashAPIKey("key4")), Admin: false}, "user_admin": {Hash: auth.APIKeyHash(auth.HashAPIKey("key5")), Admin: false}, }, } tests := []struct { apiKey string expected bool }{ {"key1", true}, // admin {"key2", true}, // admin_user {"key3", true}, // superadmin {"key4", false}, // regular {"key5", false}, // user_admin (not admin based on explicit flag) } for _, tt := range tests { t.Run(tt.apiKey, func(t *testing.T) { user, err := config.ValidateAPIKey(tt.apiKey) if err != nil { t.Fatalf("Unexpected error: %v", err) } if user.Admin != tt.expected { t.Errorf("Expected admin=%v for key %s, got %v", tt.expected, tt.apiKey, user.Admin) } }) } }