package handlers import ( "os" "path/filepath" "testing" ) // ============================================================================== // SECURITY TESTS for go-git Implementation // ============================================================================== // These tests verify that the path validation and git operations are secure. // The implementation uses go-git (pure Go) instead of exec.CommandContext // to eliminate shell command injection risks. // ============================================================================== // TestValidateRepoPath tests the security validation for repository paths func TestValidateRepoPath(t *testing.T) { // Get project root (two levels up from handlers directory) cwd, err := os.Getwd() if err != nil { t.Fatalf("Failed to get working directory: %v", err) } // Navigate to project root projectRoot := filepath.Join(cwd, "..", "..") tests := []struct { name string path string shouldErr bool errMsg string }{ { name: "Valid path within project", path: projectRoot, shouldErr: false, }, { name: "Valid subdirectory", path: filepath.Join(projectRoot, "data"), shouldErr: false, }, { name: "Path traversal attack - parent directory", path: "../../../etc/passwd", shouldErr: true, errMsg: "repository path outside project directory", }, { name: "Path traversal attack - absolute path", path: "/etc/passwd", shouldErr: true, errMsg: "repository path outside project directory", }, { name: "Command injection attempt - pipe", path: "data | cat /etc/passwd", shouldErr: true, errMsg: "path does not exist", }, { name: "Command injection attempt - semicolon", path: "data; rm -rf /", shouldErr: true, errMsg: "path does not exist", }, { name: "Command injection attempt - backticks", path: "data`whoami`", shouldErr: true, errMsg: "path does not exist", }, { name: "Non-existent path", path: filepath.Join(projectRoot, "nonexistent-directory-12345"), shouldErr: true, errMsg: "path does not exist", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateRepoPath(tt.path) if tt.shouldErr { if err == nil { t.Errorf("Expected error for path %q, got nil", tt.path) return } if tt.errMsg != "" && err.Error() != "" { // Check if error message contains expected substring if !contains(err.Error(), tt.errMsg) { t.Errorf("Expected error containing %q, got %q", tt.errMsg, err.Error()) } } } else { if err != nil { t.Errorf("Unexpected error for valid path %q: %v", tt.path, err) } } }) } } // TestGetGitRepoFirstCommitDate_SecurityValidation tests that malicious paths are rejected func TestGetGitRepoFirstCommitDate_SecurityValidation(t *testing.T) { maliciousPaths := []string{ "../../../etc/passwd", "/etc/passwd", "data | cat /etc/passwd", "data; whoami", "data`id`", "$(whoami)", } for _, path := range maliciousPaths { t.Run("Reject_"+path, func(t *testing.T) { // Should return empty string (safe rejection) result := getGitRepoFirstCommitDate(path) if result != "" { t.Errorf("Expected empty result for malicious path %q, got %q", path, result) } }) } } // TestGetGitRepoFirstCommitDate_NonGitRepo tests that non-git directories return empty func TestGetGitRepoFirstCommitDate_NonGitRepo(t *testing.T) { // Create a temporary directory that exists but is not a git repo tempDir := t.TempDir() // This should fail gracefully (not panic) result := getGitRepoFirstCommitDate(tempDir) // Should return empty string for non-git repos if result != "" { t.Errorf("Expected empty result for non-git directory, got %q", result) } } // TestGetGitRepoFirstCommitDate_ValidRepo tests the happy path with the current repo func TestGetGitRepoFirstCommitDate_ValidRepo(t *testing.T) { // Get project root (should be a valid git repo) cwd, err := os.Getwd() if err != nil { t.Fatalf("Failed to get working directory: %v", err) } projectRoot := filepath.Join(cwd, "..", "..") // This should return a valid date result := getGitRepoFirstCommitDate(projectRoot) // Should return a date in YYYY-MM format if result == "" { t.Log("Warning: No commit date returned (repo might not have commits)") return } // Validate format: YYYY-MM if len(result) != 7 || result[4] != '-' { t.Errorf("Expected date in YYYY-MM format, got %q", result) } // Year should be between 2020 and 2030 (reasonable range) year := result[:4] if year < "2020" || year > "2030" { t.Errorf("Year %s seems unreasonable for project start date", year) } t.Logf("First commit date: %s", result) } // TestFindProjectRoot tests the project root detection func TestFindProjectRoot(t *testing.T) { root, err := findProjectRoot() if err != nil { t.Fatalf("Failed to find project root: %v", err) } // Verify .git directory exists gitPath := filepath.Join(root, ".git") info, err := os.Stat(gitPath) if err != nil { t.Errorf("Expected .git directory at %s, got error: %v", gitPath, err) } if !info.IsDir() { t.Errorf("Expected .git to be a directory at %s", gitPath) } t.Logf("Project root: %s", root) } // Helper function to check if a string contains a substring func contains(s, substr string) bool { return len(s) >= len(substr) && (s == substr || len(substr) == 0 || (len(s) > 0 && len(substr) > 0 && stringContains(s, substr))) } func stringContains(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false }