package auth import ( "testing" "github.com/jfraeys/fetch_ml/internal/auth" ) func TestGenerateAPIKey(t *testing.T) { t.Parallel() // Enable parallel execution key1, hashed1, err := auth.GenerateAPIKey() if err != nil { t.Fatalf("Failed to generate API key: %v", err) } if len(key1) != 64 { // 32 bytes = 64 hex chars t.Errorf("Expected key length 64, got %d", len(key1)) } if hashed1.Algorithm != "argon2id" { t.Errorf("Expected algorithm argon2id, got %s", hashed1.Algorithm) } // Test uniqueness key2, _, err := auth.GenerateAPIKey() if err != nil { t.Fatalf("Failed to generate second API key: %v", err) } 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") } } // TestHashAPIKey_Legacy tests the legacy SHA256 hashing (kept for backward compatibility) func TestHashAPIKey_Legacy(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") } } // TestHashAPIKeyKnownValues_Legacy tests SHA256 with known hash values (backward compatibility) func TestHashAPIKeyKnownValues_Legacy(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) } }) } } // TestHashAPIKeyConsistency_Legacy tests SHA256 hash consistency (backward compatibility) func TestHashAPIKeyConsistency_Legacy(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)) } } // TestValidateAPIKey_Legacy tests API key validation using SHA256 hashes (backward compatibility) func TestValidateAPIKey_Legacy(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 TestArgon2idHashing(t *testing.T) { t.Parallel() // Generate a key and hash it with Argon2id plaintext, hashed, err := auth.GenerateAPIKey() if err != nil { t.Fatalf("Failed to generate API key: %v", err) } // Verify algorithm is argon2id if hashed.Algorithm != "argon2id" { t.Errorf("Expected algorithm argon2id, got %s", hashed.Algorithm) } // Verify hash is hex-encoded and correct length (32 bytes = 64 hex chars) if len(hashed.Hash) != 64 { t.Errorf("Expected hash length 64, got %d", len(hashed.Hash)) } // Verify salt is present and correct length (16 bytes = 32 hex chars) if len(hashed.Salt) != 32 { t.Errorf("Expected salt length 32, got %d", len(hashed.Salt)) } // Build HashedKey for verification stored := &auth.HashedKey{ Hash: hashed.Hash, Salt: hashed.Salt, Algorithm: hashed.Algorithm, } // Verify the key matches match, err := auth.VerifyAPIKey(plaintext, stored) if err != nil { t.Fatalf("VerifyAPIKey failed: %v", err) } if !match { t.Error("VerifyAPIKey should return true for matching key") } // Verify wrong key doesn't match match, err = auth.VerifyAPIKey("wrong-key", stored) if err != nil { t.Fatalf("VerifyAPIKey failed: %v", err) } if match { t.Error("VerifyAPIKey should return false for wrong key") } } func TestArgon2idDifferentSalts(t *testing.T) { t.Parallel() // Generate two keys - they should have different salts _, hashed1, _ := auth.GenerateAPIKey() _, hashed2, _ := auth.GenerateAPIKey() // Salts should be different if hashed1.Salt == hashed2.Salt { t.Error("Different API keys should have different salts") } // Hashes should be different even if same key (but they won't be same key) if hashed1.Hash == hashed2.Hash { t.Error("Different API keys should produce different hashes") } } func TestVerifyAPIKeyWithDifferentAlgorithms(t *testing.T) { t.Parallel() // Test Argon2id verification plaintext := "test-key-for-verification" hashedKey, err := auth.HashAPIKeyArgon2id(plaintext) if err != nil { t.Fatalf("Failed to hash with Argon2id: %v", err) } // Should verify successfully match, err := auth.VerifyAPIKey(plaintext, hashedKey) if err != nil { t.Fatalf("VerifyAPIKey failed: %v", err) } if !match { t.Error("Argon2id verification should succeed for correct key") } // Test with wrong algorithm sha256Stored := &auth.HashedKey{ Hash: auth.HashAPIKey(plaintext), Algorithm: "sha256", } match, err = auth.VerifyAPIKey(plaintext, sha256Stored) if err != nil { t.Fatalf("VerifyAPIKey with SHA256 failed: %v", err) } if !match { t.Error("SHA256 verification should succeed for correct key") } } func TestGenerateNewAPIKey(t *testing.T) { t.Parallel() plaintext, entry, err := auth.GenerateNewAPIKey( true, // admin []string{"admin", "operator"}, // roles map[string]bool{"*": true}, // permissions ) if err != nil { t.Fatalf("GenerateNewAPIKey failed: %v", err) } // Verify plaintext is valid if len(plaintext) != 64 { t.Errorf("Expected plaintext length 64, got %d", len(plaintext)) } // Verify entry has correct values if !entry.Admin { t.Error("Expected Admin to be true") } if len(entry.Roles) != 2 || entry.Roles[0] != "admin" { t.Errorf("Expected roles [admin operator], got %v", entry.Roles) } if entry.Algorithm != "argon2id" { t.Errorf("Expected algorithm argon2id, got %s", entry.Algorithm) } // Verify the key can be validated config := auth.Config{ Enabled: true, APIKeys: map[auth.Username]auth.APIKeyEntry{ "testuser": entry, }, } user, err := config.ValidateAPIKey(plaintext) if err != nil { t.Fatalf("Failed to validate generated key: %v", err) } if user.Name != "testuser" { t.Errorf("Expected user name testuser, got %s", user.Name) } if !user.Admin { t.Error("Expected user to be admin") } }