// Copyright (C) NHR@FAU, University Erlangen-Nuremberg. // All rights reserved. This file is part of cc-backend. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package auth import ( "testing" "github.com/ClusterCockpit/cc-lib/schema" "github.com/golang-jwt/jwt/v5" ) // TestExtractStringFromClaims tests extracting string values from JWT claims func TestExtractStringFromClaims(t *testing.T) { claims := jwt.MapClaims{ "sub": "testuser", "email": "test@example.com", "age": 25, // not a string } tests := []struct { name string key string expected string }{ {"Existing string", "sub", "testuser"}, {"Another string", "email", "test@example.com"}, {"Non-existent key", "missing", ""}, {"Non-string value", "age", ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := extractStringFromClaims(claims, tt.key) if result != tt.expected { t.Errorf("Expected %s, got %s", tt.expected, result) } }) } } // TestExtractRolesFromClaims tests role extraction and validation func TestExtractRolesFromClaims(t *testing.T) { tests := []struct { name string claims jwt.MapClaims validateRoles bool expected []string }{ { name: "Valid roles without validation", claims: jwt.MapClaims{ "roles": []any{"admin", "user", "invalid_role"}, }, validateRoles: false, expected: []string{"admin", "user", "invalid_role"}, }, { name: "Valid roles with validation", claims: jwt.MapClaims{ "roles": []any{"admin", "user", "api"}, }, validateRoles: true, expected: []string{"admin", "user", "api"}, }, { name: "Invalid roles with validation", claims: jwt.MapClaims{ "roles": []any{"invalid_role", "fake_role"}, }, validateRoles: true, expected: []string{}, // Should filter out invalid roles }, { name: "No roles claim", claims: jwt.MapClaims{}, validateRoles: false, expected: []string{}, }, { name: "Non-array roles", claims: jwt.MapClaims{ "roles": "admin", }, validateRoles: false, expected: []string{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := extractRolesFromClaims(tt.claims, tt.validateRoles) if len(result) != len(tt.expected) { t.Errorf("Expected %d roles, got %d", len(tt.expected), len(result)) return } for i, role := range result { if i >= len(tt.expected) || role != tt.expected[i] { t.Errorf("Expected role %s at position %d, got %s", tt.expected[i], i, role) } } }) } } // TestExtractProjectsFromClaims tests project extraction from claims func TestExtractProjectsFromClaims(t *testing.T) { tests := []struct { name string claims jwt.MapClaims expected []string }{ { name: "Projects as array of interfaces", claims: jwt.MapClaims{ "projects": []any{"project1", "project2", "project3"}, }, expected: []string{"project1", "project2", "project3"}, }, { name: "Projects as string array", claims: jwt.MapClaims{ "projects": []string{"projectA", "projectB"}, }, expected: []string{"projectA", "projectB"}, }, { name: "No projects claim", claims: jwt.MapClaims{}, expected: []string{}, }, { name: "Mixed types in projects array", claims: jwt.MapClaims{ "projects": []any{"project1", 123, "project2"}, }, expected: []string{"project1", "project2"}, // Should skip non-strings }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := extractProjectsFromClaims(tt.claims) if len(result) != len(tt.expected) { t.Errorf("Expected %d projects, got %d", len(tt.expected), len(result)) return } for i, project := range result { if i >= len(tt.expected) || project != tt.expected[i] { t.Errorf("Expected project %s at position %d, got %s", tt.expected[i], i, project) } } }) } } // TestExtractNameFromClaims tests name extraction from various formats func TestExtractNameFromClaims(t *testing.T) { tests := []struct { name string claims jwt.MapClaims expected string }{ { name: "Simple string name", claims: jwt.MapClaims{ "name": "John Doe", }, expected: "John Doe", }, { name: "Nested name structure", claims: jwt.MapClaims{ "name": map[string]any{ "values": []any{"John", "Doe"}, }, }, expected: "John Doe", }, { name: "Nested name with single value", claims: jwt.MapClaims{ "name": map[string]any{ "values": []any{"Alice"}, }, }, expected: "Alice", }, { name: "No name claim", claims: jwt.MapClaims{}, expected: "", }, { name: "Empty nested values", claims: jwt.MapClaims{ "name": map[string]any{ "values": []any{}, }, }, expected: "", }, { name: "Nested with non-string values", claims: jwt.MapClaims{ "name": map[string]any{ "values": []any{123, "Smith"}, }, }, expected: "123 Smith", // Should convert to string }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := extractNameFromClaims(tt.claims) if result != tt.expected { t.Errorf("Expected '%s', got '%s'", tt.expected, result) } }) } } // TestGetUserFromJWT_NoValidation tests getUserFromJWT without database validation func TestGetUserFromJWT_NoValidation(t *testing.T) { claims := jwt.MapClaims{ "sub": "testuser", "name": "Test User", "roles": []any{"user", "admin"}, "projects": []any{"project1", "project2"}, } user, err := getUserFromJWT(claims, false, schema.AuthToken, -1) if err != nil { t.Fatalf("Unexpected error: %v", err) } if user.Username != "testuser" { t.Errorf("Expected username 'testuser', got '%s'", user.Username) } if user.Name != "Test User" { t.Errorf("Expected name 'Test User', got '%s'", user.Name) } if len(user.Roles) != 2 { t.Errorf("Expected 2 roles, got %d", len(user.Roles)) } if len(user.Projects) != 2 { t.Errorf("Expected 2 projects, got %d", len(user.Projects)) } if user.AuthType != schema.AuthToken { t.Errorf("Expected AuthType %v, got %v", schema.AuthToken, user.AuthType) } } // TestGetUserFromJWT_MissingSub tests error when sub claim is missing func TestGetUserFromJWT_MissingSub(t *testing.T) { claims := jwt.MapClaims{ "name": "Test User", } _, err := getUserFromJWT(claims, false, schema.AuthToken, -1) if err == nil { t.Error("Expected error for missing sub claim") } if err.Error() != "missing 'sub' claim in JWT" { t.Errorf("Expected specific error message, got: %v", err) } }