diff --git a/.github/workflows/it-test.yml b/.github/workflows/it-test.yml index 4f80cee5..10c3140b 100644 --- a/.github/workflows/it-test.yml +++ b/.github/workflows/it-test.yml @@ -7,11 +7,32 @@ on: push: jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Needed for git history + - name: Set up Go + uses: actions/setup-go@v4 + - name: Build CLI for all platforms + run: make build-all + - name: Upload CLI binaries + uses: actions/upload-artifact@v4 + with: + name: cli-binaries + path: | + cli-v2-linux + cli-v2.exe + cli-v2-macos + test: + needs: build runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, macos-latest] # [windows-latest] removed for now + os: [ubuntu-latest, macos-latest, windows-latest] fail-fast: false steps: - name: Checkout code @@ -25,10 +46,9 @@ jobs: go-version: '1.21' cache: true - - name: Download CLI binaries from go workflow - uses: dawidd6/action-download-artifact@v2 + - name: Download CLI binaries + uses: actions/download-artifact@v4 with: - workflow: go.yml name: cli-binaries path: . @@ -48,6 +68,38 @@ jobs: if: matrix.os != 'windows-latest' run: chmod +x cli-v2 + - name: Install yq on Windows + if: matrix.os == 'windows-latest' + shell: pwsh + run: | + choco install yq -y + Import-Module $env:ChocolateyInstall\helpers\chocolateyProfile.psm1 + refreshenv + + - name: Run init tests on Windows + if: matrix.os == 'windows-latest' + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + & ./integration-tests/run.ps1 + if ($LASTEXITCODE -ne 0) { + Write-Error "Integration tests failed with exit code $LASTEXITCODE" + exit $LASTEXITCODE + } + env: + CODACY_API_TOKEN: ${{ secrets.CODACY_API_TOKEN }} + + - name: Run init tests on Unix + if: matrix.os != 'windows-latest' + id: run_init_tests_unix + continue-on-error: true + shell: bash + env: + CODACY_API_TOKEN: ${{ secrets.CODACY_API_TOKEN }} + run: | + chmod +x integration-tests/run.sh + ./integration-tests/run.sh + - name: Run tool tests if: matrix.os != 'windows-latest' id: run_tests @@ -84,7 +136,7 @@ jobs: fi - name: Check test results - if: steps.run_tests.outcome == 'failure' + if: failure() run: | - echo "Job failed because some tool tests failed. Please check the logs above for details." + echo "Job failed because some tests failed. Please check the logs above for details." exit 1 \ No newline at end of file diff --git a/cmd/init.go b/cmd/init.go index e10471ee..4bcace14 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -75,8 +75,8 @@ var initCmd = &cobra.Command{ if err != nil { log.Fatal(err) } - createGitIgnoreFile() } + createGitIgnoreFile() fmt.Println() fmt.Println("✅ Successfully initialized Codacy configuration!") fmt.Println() @@ -95,12 +95,7 @@ func createGitIgnoreFile() error { } defer gitIgnoreFile.Close() - content := `# Codacy CLI -tools-configs/ -.gitignore -cli-config.yaml -logs/ -` + content := "# Codacy CLI\ntools-configs/\n.gitignore\ncli-config.yaml\nlogs/\n" if _, err := gitIgnoreFile.WriteString(content); err != nil { return fmt.Errorf("failed to write to .gitignore file: %w", err) } @@ -275,6 +270,8 @@ func buildRepositoryConfigurationFiles(token string) error { PyLint: "pylint", PMD: "pmd", DartAnalyzer: "dartanalyzer", + Lizard: "lizard", + Semgrep: "semgrep", } // Generate languages configuration based on API tools response @@ -384,45 +381,48 @@ func createToolFileConfigurations(tool tools.Tool, patternConfiguration []domain if err != nil { return fmt.Errorf("failed to create Trivy config: %v", err) } + fmt.Println("Trivy configuration created based on Codacy settings") } else { err := createDefaultTrivyConfigFile(toolsConfigDir) if err != nil { return fmt.Errorf("failed to create default Trivy config: %v", err) } } - fmt.Println("Trivy configuration created based on Codacy settings") case PMD: if len(patternConfiguration) > 0 { err := createPMDConfigFile(patternConfiguration, toolsConfigDir) if err != nil { return fmt.Errorf("failed to create PMD config: %v", err) } + + fmt.Println("PMD configuration created based on Codacy settings") } else { err := createDefaultPMDConfigFile(toolsConfigDir) if err != nil { return fmt.Errorf("failed to create default PMD config: %v", err) } } - fmt.Println("PMD configuration created based on Codacy settings") + case PyLint: if len(patternConfiguration) > 0 { err := createPylintConfigFile(patternConfiguration, toolsConfigDir) if err != nil { return fmt.Errorf("failed to create Pylint config: %v", err) } + fmt.Println("Pylint configuration created based on Codacy settings") } else { err := createDefaultPylintConfigFile(toolsConfigDir) if err != nil { return fmt.Errorf("failed to create default Pylint config: %v", err) } } - fmt.Println("Pylint configuration created based on Codacy settings") case DartAnalyzer: if len(patternConfiguration) > 0 { err := createDartAnalyzerConfigFile(patternConfiguration, toolsConfigDir) if err != nil { return fmt.Errorf("failed to create Dart Analyzer config: %v", err) } + fmt.Println("Dart configuration created based on Codacy settings") } case Semgrep: if len(patternConfiguration) > 0 { @@ -430,6 +430,7 @@ func createToolFileConfigurations(tool tools.Tool, patternConfiguration []domain if err != nil { return fmt.Errorf("failed to create Semgrep config: %v", err) } + fmt.Println("Semgrep configuration created based on Codacy settings") } case Lizard: createLizardConfigFile(toolsConfigDir, patternConfiguration) @@ -541,7 +542,6 @@ func createLizardConfigFile(toolsConfigDir string, patternConfiguration []domain var patterns []domain.PatternDefinition if len(patternConfiguration) == 0 { - fmt.Println("Using default Lizard configuration") var err error patterns, err = tools.FetchDefaultEnabledPatterns(Lizard) if err != nil { diff --git a/integration-tests/init-with-token/expected/.codacy/codacy.yaml b/integration-tests/init-with-token/expected/.codacy/codacy.yaml new file mode 100644 index 00000000..3fef092a --- /dev/null +++ b/integration-tests/init-with-token/expected/.codacy/codacy.yaml @@ -0,0 +1,10 @@ +runtimes: + - node@22.2.0 + - python@3.11.11 +tools: + - semgrep@1.78.0 + - lizard@1.17.19 + - eslint@8.57.0 + - trivy@0.59.1 + - pylint@3.3.6 + - pmd@6.55.0 diff --git a/integration-tests/init-with-token/expected/.gitignore b/integration-tests/init-with-token/expected/.gitignore new file mode 100644 index 00000000..780308b3 --- /dev/null +++ b/integration-tests/init-with-token/expected/.gitignore @@ -0,0 +1,5 @@ +# Codacy CLI +tools-configs/ +.gitignore +cli-config.yaml +logs/ diff --git a/integration-tests/init-with-token/expected/cli-config.yaml b/integration-tests/init-with-token/expected/cli-config.yaml new file mode 100644 index 00000000..644407e1 --- /dev/null +++ b/integration-tests/init-with-token/expected/cli-config.yaml @@ -0,0 +1 @@ +mode: remote \ No newline at end of file diff --git a/integration-tests/init-with-token/expected/codacy.yaml b/integration-tests/init-with-token/expected/codacy.yaml new file mode 100644 index 00000000..5da91080 --- /dev/null +++ b/integration-tests/init-with-token/expected/codacy.yaml @@ -0,0 +1,10 @@ +runtimes: + - node@22.2.0 + - python@3.11.11 +tools: + - eslint@8.57.0 + - trivy@0.59.1 + - pylint@3.3.6 + - pmd@6.55.0 + - semgrep@1.78.0 + - lizard@1.17.19 diff --git a/integration-tests/init-with-token/expected/tools-configs/eslint.config.mjs b/integration-tests/init-with-token/expected/tools-configs/eslint.config.mjs new file mode 100644 index 00000000..b5931786 --- /dev/null +++ b/integration-tests/init-with-token/expected/tools-configs/eslint.config.mjs @@ -0,0 +1,6 @@ +export default [ + { + rules: { + } + } +]; \ No newline at end of file diff --git a/integration-tests/init-with-token/expected/tools-configs/languages-config.yaml b/integration-tests/init-with-token/expected/tools-configs/languages-config.yaml new file mode 100644 index 00000000..9619a373 --- /dev/null +++ b/integration-tests/init-with-token/expected/tools-configs/languages-config.yaml @@ -0,0 +1,19 @@ +tools: + - name: pylint + languages: [Python] + extensions: [.py] + - name: lizard + languages: [Java, JavaScript, Python] + extensions: [.java, .js, .jsm, .jsx, .mjs, .py, .vue] + - name: pmd + languages: [Java, JavaScript] + extensions: [.java, .js, .jsm, .jsx, .mjs, .vue] + - name: eslint + languages: [JavaScript] + extensions: [.js, .jsm, .jsx, .mjs, .vue] + - name: trivy + languages: [Multiple] + extensions: [] + - name: semgrep + languages: [Java, JavaScript, JSON, Python] + extensions: [.java, .js, .jsm, .json, .jsx, .mjs, .py, .vue] diff --git a/integration-tests/init-with-token/expected/tools-configs/lizard.yaml b/integration-tests/init-with-token/expected/tools-configs/lizard.yaml new file mode 100644 index 00000000..87f8b46c --- /dev/null +++ b/integration-tests/init-with-token/expected/tools-configs/lizard.yaml @@ -0,0 +1,40 @@ +patterns: + Lizard_ccn-minor: + category: Complexity + description: Check the Cyclomatic Complexity value of a function or logic block. If the threshold is not met, raise a Minor issue. The default threshold is 5. + explanation: |- + # Minor Cyclomatic Complexity control + + Check the Cyclomatic Complexity value of a function or logic block. If the threshold is not met, raise a Minor issue. The default threshold is 4. + id: Lizard_ccn-minor + level: Info + severityLevel: Info + threshold: 5 + timeToFix: 5 + title: Minor Cyclomatic Complexity control + Lizard_nloc-critical: + category: Complexity + description: Check the number of lines of code (without comments) in a function or logic block. If the threshold is not met, raise a Critical issue. The default threshold is 100. + explanation: |- + # Critical NLOC control - Number of Lines of Code (without comments) + + Check the number of lines of code (without comments) in a function or logic block. If the threshold is not met, raise a Critical issue. The default threshold is 100. + id: Lizard_nloc-critical + level: Error + severityLevel: Error + threshold: 100 + timeToFix: 5 + title: Critical NLOC control - Number of Lines of Code (without comments) + Lizard_nloc-medium: + category: Complexity + description: Check the number of lines of code (without comments) in a function. If the threshold is not met, raise a Medium issue. The default threshold is 50. + explanation: |- + # Medium NLOC control - Number of Lines of Code (without comments) + + Check the number of lines of code (without comments) in a function. If the threshold is not met, raise a Medium issue. The default threshold is 50. + id: Lizard_nloc-medium + level: Warning + severityLevel: Warning + threshold: 50 + timeToFix: 5 + title: Medium NLOC control - Number of Lines of Code (without comments) diff --git a/integration-tests/init-with-token/expected/tools-configs/pylint.rc b/integration-tests/init-with-token/expected/tools-configs/pylint.rc new file mode 100644 index 00000000..2047c82e --- /dev/null +++ b/integration-tests/init-with-token/expected/tools-configs/pylint.rc @@ -0,0 +1,9 @@ +[MASTER] +ignore=CVS +persistent=yes +load-plugins= + +[MESSAGES CONTROL] +disable=all +enable=E1124,E1130,E1133 + diff --git a/integration-tests/init-with-token/expected/tools-configs/ruleset.xml b/integration-tests/init-with-token/expected/tools-configs/ruleset.xml new file mode 100644 index 00000000..bcbd424f --- /dev/null +++ b/integration-tests/init-with-token/expected/tools-configs/ruleset.xml @@ -0,0 +1,12 @@ + + + Codacy PMD Ruleset + + + + + + \ No newline at end of file diff --git a/integration-tests/init-with-token/expected/tools-configs/semgrep.yaml b/integration-tests/init-with-token/expected/tools-configs/semgrep.yaml new file mode 100644 index 00000000..f7fdd3f9 --- /dev/null +++ b/integration-tests/init-with-token/expected/tools-configs/semgrep.yaml @@ -0,0 +1,69 @@ +rules: + - id: apex.lang.security.ncino.endpoints.namedcredentialsconstantmatch.named-credentials-constant-match + languages: + - apex + message: Named Credentials (and callout endpoints) should be used instead of hard-coding credentials. 1. Hard-coded credentials are hard to maintain when mixed in with application code. 2. It is particularly hard to update hard-coded credentials when they are used amongst different classes. 3. Granting a developer access to the codebase means granting knowledge of credentials, and thus keeping a two-level access is not possible. 4. Using different credentials for different environments is troublesome and error-prone. + metadata: + category: security + confidence: HIGH + cwe: + - 'CWE-540: Inclusion of Sensitive Information in Source Code' + impact: HIGH + likelihood: LOW + references: + - https://cwe.mitre.org/data/definitions/540.html + subcategory: + - vuln + technology: + - salesforce + min-version: 1.44.0 + mode: taint + pattern-sinks: + - patterns: + - pattern: req.setHeader($X, ...); + - focus-metavariable: $X + pattern-sources: + - pattern: '...String $X = ''Authorization'';' + severity: ERROR + - id: clojure.lang.security.use-of-md5.use-of-md5 + languages: + - clojure + message: MD5 hash algorithm detected. This is not collision resistant and leads to easily-cracked password hashes. Replace with current recommended hashing algorithms. + metadata: + author: Gabriel Marquet + category: security + confidence: HIGH + cwe: + - 'CWE-328: Use of Weak Hash' + impact: HIGH + likelihood: MEDIUM + owasp: + - A03:2017 - Sensitive Data Exposure + - A02:2021 - Cryptographic Failures + references: + - https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html + - https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html + source-rule-url: https://github.com/clj-holmes/clj-holmes-rules/blob/main/security/weak-hash-function-md5.yml + subcategory: + - vuln + technology: + - clojure + pattern-either: + - pattern: (MessageDigest/getInstance "MD5") + - pattern: (MessageDigest/getInstance MessageDigestAlgorithms/MD5) + - pattern: (MessageDigest/getInstance org.apache.commons.codec.digest.MessageDigestAlgorithms/MD5) + - pattern: (java.security.MessageDigest/getInstance "MD5") + - pattern: (java.security.MessageDigest/getInstance MessageDigestAlgorithms/MD5) + - pattern: (java.security.MessageDigest/getInstance org.apache.commons.codec.digest.MessageDigestAlgorithms/MD5) + severity: WARNING + - id: codacy.generic.plsql.empty-strings + languages: + - generic + message: Empty strings can lead to unexpected behavior and should be handled carefully. + metadata: + category: security + confidence: MEDIUM + description: Detects empty strings in the code which might cause issues or bugs. + impact: MEDIUM + pattern: $VAR VARCHAR2($LENGTH) := ''; + severity: WARNING diff --git a/integration-tests/init-with-token/expected/tools-configs/trivy.yaml b/integration-tests/init-with-token/expected/tools-configs/trivy.yaml new file mode 100644 index 00000000..c785541c --- /dev/null +++ b/integration-tests/init-with-token/expected/tools-configs/trivy.yaml @@ -0,0 +1,10 @@ +severity: + - LOW + - MEDIUM + - HIGH + - CRITICAL + +scan: + scanners: + - vuln + - secret diff --git a/integration-tests/init-without-token/expected/.gitignore b/integration-tests/init-without-token/expected/.gitignore new file mode 100644 index 00000000..780308b3 --- /dev/null +++ b/integration-tests/init-without-token/expected/.gitignore @@ -0,0 +1,5 @@ +# Codacy CLI +tools-configs/ +.gitignore +cli-config.yaml +logs/ diff --git a/integration-tests/init-without-token/expected/cli-config.yaml b/integration-tests/init-without-token/expected/cli-config.yaml new file mode 100644 index 00000000..6ae4b29d --- /dev/null +++ b/integration-tests/init-without-token/expected/cli-config.yaml @@ -0,0 +1 @@ +mode: local \ No newline at end of file diff --git a/integration-tests/init-without-token/expected/codacy.yaml b/integration-tests/init-without-token/expected/codacy.yaml new file mode 100644 index 00000000..a548d1b4 --- /dev/null +++ b/integration-tests/init-without-token/expected/codacy.yaml @@ -0,0 +1,12 @@ +runtimes: + - node@22.2.0 + - python@3.11.11 + - dart@3.7.2 +tools: + - eslint@9.3.0 + - trivy@0.59.1 + - pylint@3.3.6 + - pmd@6.55.0 + - dartanalyzer@3.7.2 + - semgrep@1.78.0 + - lizard@1.17.19 diff --git a/integration-tests/init-without-token/expected/tools-configs/lizard.yaml b/integration-tests/init-without-token/expected/tools-configs/lizard.yaml new file mode 100644 index 00000000..66964fde --- /dev/null +++ b/integration-tests/init-without-token/expected/tools-configs/lizard.yaml @@ -0,0 +1,50 @@ +patterns: + Lizard_ccn-medium: + category: Complexity + description: Check the Cyclomatic Complexity value of a function or logic block. If the threshold is not met, raise a Medium issue. The default threshold is 8. + explanation: |- + # Medium Cyclomatic Complexity control + + Check the Cyclomatic Complexity value of a function or logic block. If the threshold is not met, raise a Medium issue. The default threshold is 7. + id: Lizard_ccn-medium + level: Warning + severityLevel: Warning + threshold: 8 + timeToFix: 5 + title: Medium Cyclomatic Complexity control + Lizard_file-nloc-medium: + category: Complexity + description: Check the number of lines of code (without comments) in a file. If the threshold is not met, raise a Medium issue. The default threshold is 500. + explanation: "" + id: Lizard_file-nloc-medium + level: Warning + severityLevel: Warning + threshold: 500 + timeToFix: 5 + title: Medium File NLOC control - Number of Lines of Code (without comments) + Lizard_nloc-medium: + category: Complexity + description: Check the number of lines of code (without comments) in a function. If the threshold is not met, raise a Medium issue. The default threshold is 50. + explanation: |- + # Medium NLOC control - Number of Lines of Code (without comments) + + Check the number of lines of code (without comments) in a function. If the threshold is not met, raise a Medium issue. The default threshold is 50. + id: Lizard_nloc-medium + level: Warning + severityLevel: Warning + threshold: 50 + timeToFix: 5 + title: Medium NLOC control - Number of Lines of Code (without comments) + Lizard_parameter-count-medium: + category: Complexity + description: Check the number of parameters sent to a function. If the threshold is not met, raise a Medium issue. The default threshold is 8. + explanation: |- + # Medium Parameter count control + + Check the number of parameters sent to a function. If the threshold is not met, raise a Medium issue. The default threshold is 5. + id: Lizard_parameter-count-medium + level: Warning + severityLevel: Warning + threshold: 8 + timeToFix: 5 + title: Medium Parameter count control diff --git a/integration-tests/run.ps1 b/integration-tests/run.ps1 new file mode 100644 index 00000000..018e84c4 --- /dev/null +++ b/integration-tests/run.ps1 @@ -0,0 +1,129 @@ +# Stop on first error +$ErrorActionPreference = "Stop" + +# Get the absolute path of the script's directory and CLI path +$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path +$CLI_PATH = Join-Path (Get-Location) "cli-v2.exe" + +Write-Host "Script directory: $SCRIPT_DIR" +Write-Host "Current working directory: $(Get-Location)" + +# Check if API token is provided for token-based test +if (-not $env:CODACY_API_TOKEN) { + Write-Host "Warning: CODACY_API_TOKEN environment variable is not set. Token-based test will be skipped." +} + +# Function to normalize and sort configuration values +function Normalize-Config { + param ([string]$file) + + $ext = [System.IO.Path]::GetExtension($file).TrimStart('.') + + switch ($ext) { + { $_ -in @('yaml', 'yml') } { yq e '.' $file | Sort-Object } + { $_ -in @('rc', 'conf', 'ini', 'xml') } { + Get-Content $file | ForEach-Object { + if ($_ -match '^[^#].*=.*,') { + $parts = $_ -split '=' + $values = $parts[1] -split ',' | Sort-Object + "$($parts[0])=$($values -join ',')" + } else { $_ } + } | Sort-Object + } + default { Get-Content $file | Sort-Object } + } +} + +function Compare-Files { + param ( + [string]$expectedDir, + [string]$actualDir, + [string]$label + ) + + # Compare files + Get-ChildItem -Path $expectedDir -File | ForEach-Object { + $actualFile = Join-Path $actualDir $_.Name + if (-not (Test-Path $actualFile)) { + Write-Host "❌ $label/$($_.Name) does not exist in actual output" + Write-Host "Expected: $($_.FullName)" + Write-Host "Actual should be: $actualFile" + exit 1 + } + + $expectedContent = Normalize-Config $_.FullName + $actualContent = Normalize-Config $actualFile + + if (Compare-Object $expectedContent $actualContent) { + Write-Host "❌ $label/$($_.Name) does not match expected" + Write-Host "=== Expected (normalized) ===" + $expectedContent + Write-Host "=== Actual (normalized) ===" + $actualContent + Write-Host "=== Diff ===" + Compare-Object $expectedContent $actualContent + Write-Host "===================" + exit 1 + } + Write-Host "✅ $label/$($_.Name) matches expected" + } + + # Compare subdirectories + Get-ChildItem -Path $expectedDir -Directory | Where-Object { $_.Name -ne "logs" } | ForEach-Object { + $actualSubDir = if ($_.Name -eq ".codacy") { $actualDir } else { Join-Path $actualDir $_.Name } + + if (-not (Test-Path $actualSubDir)) { + Write-Host "❌ Directory $label/$($_.Name) does not exist in actual output" + Write-Host "Expected: $($_.FullName)" + Write-Host "Actual should be: $actualSubDir" + exit 1 + } + Compare-Files $_.FullName $actualSubDir "$label/$($_.Name)" + } +} + +function Run-InitTest { + param ( + [string]$testDir, + [string]$testName, + [bool]$useToken + ) + + Write-Host "Running test: $testName" + if (-not (Test-Path $testDir)) { + Write-Host "❌ Test directory does not exist: $testDir" + exit 1 + } + + $originalLocation = Get-Location + try { + Set-Location $testDir + if (Test-Path ".codacy") { Remove-Item -Recurse -Force ".codacy" } + + if ($useToken) { + if (-not $env:CODACY_API_TOKEN) { + Write-Host "❌ Skipping token-based test: CODACY_API_TOKEN not set" + return + } + & $CLI_PATH init --api-token $env:CODACY_API_TOKEN --organization troubleshoot-codacy-dev --provider gh --repository codacy-cli-test + } else { + & $CLI_PATH init + } + + Compare-Files "expected" ".codacy" "Test $testName" + Write-Host "✅ Test $testName completed successfully" + Write-Host "----------------------------------------" + } + finally { + Set-Location $originalLocation + } +} + +# Run tests +Write-Host "Starting integration tests..." +Write-Host "----------------------------------------" + +Run-InitTest (Join-Path $SCRIPT_DIR "init-without-token") "init-without-token" $false +Run-InitTest (Join-Path $SCRIPT_DIR "init-with-token") "init-with-token" $true + +Write-Host "All tests completed successfully! 🎉" \ No newline at end of file diff --git a/integration-tests/run.sh b/integration-tests/run.sh new file mode 100644 index 00000000..8ff17388 --- /dev/null +++ b/integration-tests/run.sh @@ -0,0 +1,133 @@ +#!/bin/bash +set -e + +# Get the absolute path of the script's directory +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +CLI_PATH="$(pwd)/cli-v2" + +echo "Script directory: $SCRIPT_DIR" +echo "Current working directory: $(pwd)" + +# Check if API token is provided for token-based test +if [ -z "$CODACY_API_TOKEN" ]; then + echo "Warning: CODACY_API_TOKEN environment variable is not set. Token-based test will be skipped." +fi + +# Function to normalize and sort configuration values +normalize_config() { + local file=$1 + local ext="${file##*.}" + + case "$ext" in + yaml|yml) + # For YAML files, use yq to sort + yq e '.' "$file" | sort + ;; + rc|conf|ini|xml) + # For other config files, sort values after '=' and keep other lines + awk -F'=' ' + /^[^#].*=.*,/ { + split($2, values, ",") + asort(values) + printf "%s=", $1 + for (i=1; i<=length(values); i++) { + if (i>1) printf "," + printf "%s", values[i] + } + print "" + next + } + { print } + ' "$file" | sort + ;; + *) + # For other files, just sort + sort "$file" + ;; + esac +} + +compare_files() { + local expected_dir="$1" + local actual_dir="$2" + local label="$3" + + # Compare files in current directory + for file in "$expected_dir"/*; do + [ -f "$file" ] || continue + filename=$(basename "$file") + actual_file="$actual_dir/$filename" + + if [ ! -f "$actual_file" ]; then + echo "❌ $label/$filename does not exist in actual output" + echo "Expected: $file" + echo "Actual should be: $actual_file" + exit 1 + fi + + if diff <(normalize_config "$file") <(normalize_config "$actual_file") >/dev/null 2>&1; then + echo "✅ $label/$filename matches expected" + else + echo "❌ $label/$filename does not match expected" + echo "=== Expected (normalized) ===" + normalize_config "$file" + echo "=== Actual (normalized) ===" + normalize_config "$actual_file" + echo "=== Diff ===" + diff <(normalize_config "$file") <(normalize_config "$actual_file") || true + echo "===================" + exit 1 + fi + done + + # Compare subdirectories + for dir in "$expected_dir"/*/; do + [ -d "$dir" ] || continue + dirname=$(basename "$dir") + [ "$dirname" = "logs" ] && continue + + if [ ! -d "$actual_dir/$dirname" ]; then + echo "❌ Directory $label/$dirname does not exist in actual output" + echo "Expected: $dir" + echo "Actual should be: $actual_dir/$dirname" + exit 1 + fi + compare_files "$dir" "$actual_dir/$dirname" "$label/$dirname" + done +} + +run_init_test() { + local test_dir="$1" + local test_name="$2" + local use_token="$3" + + echo "Running test: $test_name" + [ -d "$test_dir" ] || { echo "❌ Test directory does not exist: $test_dir"; exit 1; } + + cd "$test_dir" || exit 1 + rm -rf .codacy + + if [ "$use_token" = "true" ]; then + [ -n "$CODACY_API_TOKEN" ] || { echo "❌ Skipping token-based test: CODACY_API_TOKEN not set"; return 0; } + "$CLI_PATH" init --api-token "$CODACY_API_TOKEN" --organization troubleshoot-codacy-dev --provider gh --repository codacy-cli-test + else + "$CLI_PATH" init + fi + + compare_files "expected" ".codacy" "Test $test_name" + echo "✅ Test $test_name completed successfully" + echo "----------------------------------------" +} + +# Run both tests +echo "Starting integration tests..." +echo "----------------------------------------" + +# Test 1: Init without token +run_init_test "$SCRIPT_DIR/init-without-token" "init-without-token" "false" + +# Test 2: Init with token +run_init_test "$SCRIPT_DIR/init-with-token" "init-with-token" "true" + +echo "All tests completed successfully! 🎉" + diff --git a/tools/dartanalyzerRunner_test.go b/tools/dartanalyzerRunner_test.go deleted file mode 100644 index fb0b8665..00000000 --- a/tools/dartanalyzerRunner_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package tools - -import ( - "log" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRunDartAnalyzerToFile(t *testing.T) { - homeDirectory, err := os.UserHomeDir() - if err != nil { - log.Fatal(err.Error()) - } - currentDirectory, err := os.Getwd() - if err != nil { - log.Fatal(err.Error()) - } - testDirectory := "testdata/repositories/dartanalizer" - tempResultFile := filepath.Join(os.TempDir(), "eslint.sarif") - defer os.Remove(tempResultFile) - - repositoryToAnalyze := filepath.Join(testDirectory, "src") - expectedSarifFile := filepath.Join(testDirectory, "expected.sarif") - dartInstallationDirectory := filepath.Join(homeDirectory, ".cache/codacy/runtimes/dart-sdk") - dartBinary := "dart" - - RunDartAnalyzer(repositoryToAnalyze, dartInstallationDirectory, dartBinary, nil, tempResultFile, "sarif") - - expectedSarifBytes, err := os.ReadFile(expectedSarifFile) - if err != nil { - log.Fatal(err) - } - - obtainedSarifBytes, err := os.ReadFile(tempResultFile) - if err != nil { - log.Fatal(err.Error()) - } - obtainedSarif := string(obtainedSarifBytes) - - filePrefix := currentDirectory + "/" - actualSarif := strings.ReplaceAll(obtainedSarif, filePrefix, "") - - expectedSarif := strings.TrimSpace(string(expectedSarifBytes)) - - assert.Equal(t, expectedSarif, actualSarif, "output did not match expected") -} diff --git a/tools/enigmaRunner_test.go b/tools/enigmaRunner_test.go deleted file mode 100644 index fbf1ebca..00000000 --- a/tools/enigmaRunner_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package tools - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRunEnigma_NoConfig_NoOutputFile(t *testing.T) { - tempDir := t.TempDir() - fakeBinary := "/bin/echo" - err := RunEnigma(tempDir, tempDir, fakeBinary, []string{"foo.go"}, "", "text") - assert.NoError(t, err) -} - -func TestRunEnigma_WithConfig_NoOutputFile(t *testing.T) { - tempDir := t.TempDir() - configPath := filepath.Join(tempDir, "enigma.yaml") - ioutil.WriteFile(configPath, []byte("config: true"), 0644) - fakeBinary := "/bin/echo" - err := RunEnigma(tempDir, tempDir, fakeBinary, []string{"foo.go"}, "", "text") - assert.NoError(t, err) -} - -func TestRunEnigma_NoConfig_WithOutputFile(t *testing.T) { - tempDir := t.TempDir() - outputFile := filepath.Join(tempDir, "output.txt") - fakeBinary := "/bin/echo" - err := RunEnigma(tempDir, tempDir, fakeBinary, []string{"foo.go"}, outputFile, "text") - assert.NoError(t, err) - _, err = os.Stat(outputFile) - assert.NoError(t, err) -} - -func TestRunEnigma_WithConfig_WithOutputFile(t *testing.T) { - tempDir := t.TempDir() - configPath := filepath.Join(tempDir, "enigma.yaml") - ioutil.WriteFile(configPath, []byte("config: true"), 0644) - outputFile := filepath.Join(tempDir, "output.txt") - fakeBinary := "/bin/echo" - err := RunEnigma(tempDir, tempDir, fakeBinary, []string{"foo.go"}, outputFile, "text") - assert.NoError(t, err) - _, err = os.Stat(outputFile) - assert.NoError(t, err) -} - -func TestRunEnigma_CreateOutputFileError(t *testing.T) { - tempDir := t.TempDir() - fakeBinary := "/bin/echo" - // Use a directory as output file to force error - outputFile := tempDir - err := RunEnigma(tempDir, tempDir, fakeBinary, []string{"foo.go"}, outputFile, "text") - assert.Error(t, err) -} diff --git a/tools/eslintRunner_test.go b/tools/eslintRunner_test.go deleted file mode 100644 index a0fd91d0..00000000 --- a/tools/eslintRunner_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package tools - -import ( - "log" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRunEslintToFile(t *testing.T) { - homeDirectory, err := os.UserHomeDir() - if err != nil { - log.Fatal(err.Error()) - } - currentDirectory, err := os.Getwd() - if err != nil { - log.Fatal(err.Error()) - } - testDirectory := "testdata/repositories/test1" - tempResultFile := filepath.Join(os.TempDir(), "eslint.sarif") - defer os.Remove(tempResultFile) - - repositoryToAnalyze := filepath.Join(testDirectory, "src") - expectedSarifFile := filepath.Join(testDirectory, "expected.sarif") - eslintInstallationDirectory := filepath.Join(homeDirectory, ".cache/codacy/tools/eslint@8.57.0") - nodeBinary := "node" - - RunEslint(repositoryToAnalyze, eslintInstallationDirectory, nodeBinary, nil, false, tempResultFile, "sarif") - - expectedSarifBytes, err := os.ReadFile(expectedSarifFile) - if err != nil { - log.Fatal(err) - } - - obtainedSarifBytes, err := os.ReadFile(tempResultFile) - if err != nil { - log.Fatal(err.Error()) - } - obtainedSarif := string(obtainedSarifBytes) - filePrefix := "file://" + currentDirectory + "/" - actualSarif := strings.ReplaceAll(obtainedSarif, filePrefix, "") - - expectedSarif := strings.TrimSpace(string(expectedSarifBytes)) - - assert.Equal(t, expectedSarif, actualSarif, "output did not match expected") -} diff --git a/tools/getTools.go b/tools/getTools.go index 42ee4c26..e9828ace 100644 --- a/tools/getTools.go +++ b/tools/getTools.go @@ -135,18 +135,18 @@ type Tool struct { Name string `json:"name"` Version string `json:"version"` Settings struct { - Enabled bool `json:"isEnabled"` - UsesConfigFile bool `json:"hasConfigurationFile"` + Enabled bool `json:"isEnabled"` + HasConfigurationFile bool `json:"hasConfigurationFile"` + UsesConfigurationFile bool `json:"usesConfigurationFile"` } `json:"settings"` } // FilterToolsByConfigUsage filters out tools that use their own configuration files -// Returns only tools that need configuration to be generated for them (UsesConfigFile = false) +// Returns only tools that need configuration to be generated for them (UsesConfigurationFile = false) func FilterToolsByConfigUsage(tools []Tool) []Tool { var filtered []Tool for _, tool := range tools { - - if !tool.Settings.UsesConfigFile { + if !tool.Settings.UsesConfigurationFile { filtered = append(filtered, tool) } else { fmt.Printf("Skipping config generation for %s - configured to use repo's config file\n", tool.Name) diff --git a/tools/getTools_test.go b/tools/getTools_test.go index 0a38fb8f..07b1558a 100644 --- a/tools/getTools_test.go +++ b/tools/getTools_test.go @@ -14,39 +14,45 @@ func TestFilterToolsByConfigUsage(t *testing.T) { expectedTools []string }{ { - name: "tools with UsesConfigFile=true should be filtered out", + name: "tools with UsesConfigurationFile=true should be filtered out", inputTools: []Tool{ { Uuid: "eslint-uuid", Name: "eslint", Settings: struct { - Enabled bool `json:"isEnabled"` - UsesConfigFile bool `json:"hasConfigurationFile"` + Enabled bool `json:"isEnabled"` + HasConfigurationFile bool `json:"hasConfigurationFile"` + UsesConfigurationFile bool `json:"usesConfigurationFile"` }{ - Enabled: true, - UsesConfigFile: true, + Enabled: true, + HasConfigurationFile: true, + UsesConfigurationFile: true, }, }, { Uuid: "trivy-uuid", Name: "trivy", Settings: struct { - Enabled bool `json:"isEnabled"` - UsesConfigFile bool `json:"hasConfigurationFile"` + Enabled bool `json:"isEnabled"` + HasConfigurationFile bool `json:"hasConfigurationFile"` + UsesConfigurationFile bool `json:"usesConfigurationFile"` }{ - Enabled: true, - UsesConfigFile: false, + Enabled: true, + HasConfigurationFile: false, + UsesConfigurationFile: false, }, }, { Uuid: "pylint-uuid", Name: "pylint", Settings: struct { - Enabled bool `json:"isEnabled"` - UsesConfigFile bool `json:"hasConfigurationFile"` + Enabled bool `json:"isEnabled"` + HasConfigurationFile bool `json:"hasConfigurationFile"` + UsesConfigurationFile bool `json:"usesConfigurationFile"` }{ - Enabled: true, - UsesConfigFile: false, + Enabled: true, + HasConfigurationFile: false, + UsesConfigurationFile: false, }, }, }, @@ -60,22 +66,26 @@ func TestFilterToolsByConfigUsage(t *testing.T) { Uuid: "eslint-uuid", Name: "eslint", Settings: struct { - Enabled bool `json:"isEnabled"` - UsesConfigFile bool `json:"hasConfigurationFile"` + Enabled bool `json:"isEnabled"` + HasConfigurationFile bool `json:"hasConfigurationFile"` + UsesConfigurationFile bool `json:"usesConfigurationFile"` }{ - Enabled: true, - UsesConfigFile: true, + Enabled: true, + HasConfigurationFile: true, + UsesConfigurationFile: true, }, }, { Uuid: "trivy-uuid", Name: "trivy", Settings: struct { - Enabled bool `json:"isEnabled"` - UsesConfigFile bool `json:"hasConfigurationFile"` + Enabled bool `json:"isEnabled"` + HasConfigurationFile bool `json:"hasConfigurationFile"` + UsesConfigurationFile bool `json:"usesConfigurationFile"` }{ - Enabled: true, - UsesConfigFile: true, + Enabled: true, + HasConfigurationFile: true, + UsesConfigurationFile: true, }, }, }, @@ -89,22 +99,26 @@ func TestFilterToolsByConfigUsage(t *testing.T) { Uuid: "eslint-uuid", Name: "eslint", Settings: struct { - Enabled bool `json:"isEnabled"` - UsesConfigFile bool `json:"hasConfigurationFile"` + Enabled bool `json:"isEnabled"` + HasConfigurationFile bool `json:"hasConfigurationFile"` + UsesConfigurationFile bool `json:"usesConfigurationFile"` }{ - Enabled: true, - UsesConfigFile: false, + Enabled: true, + HasConfigurationFile: true, + UsesConfigurationFile: false, }, }, { Uuid: "pylint-uuid", Name: "pylint", Settings: struct { - Enabled bool `json:"isEnabled"` - UsesConfigFile bool `json:"hasConfigurationFile"` + Enabled bool `json:"isEnabled"` + HasConfigurationFile bool `json:"hasConfigurationFile"` + UsesConfigurationFile bool `json:"usesConfigurationFile"` }{ - Enabled: true, - UsesConfigFile: false, + Enabled: true, + HasConfigurationFile: false, + UsesConfigurationFile: false, }, }, }, @@ -134,10 +148,10 @@ func TestFilterToolsByConfigUsage(t *testing.T) { assert.True(t, found, "Expected tool %s not found in filtered results", expectedTool) } - // Verify no tools with UsesConfigFile=true are in the result + // Verify no tools with UsesConfigurationFile=true are in the result for _, tool := range result { - assert.False(t, tool.Settings.UsesConfigFile, - "Tool %s with UsesConfigFile=true should not be in filtered results", tool.Name) + assert.False(t, tool.Settings.UsesConfigurationFile, + "Tool %s with UsesConfigurationFile=true should not be in filtered results", tool.Name) } }) } diff --git a/tools/language_config.go b/tools/language_config.go index a1cd802e..4242018f 100644 --- a/tools/language_config.go +++ b/tools/language_config.go @@ -64,6 +64,16 @@ func CreateLanguagesConfigFile(apiTools []Tool, toolsConfigDir string, toolIDMap Languages: []string{"Dart"}, Extensions: []string{".dart"}, }, + "lizard": { + Name: "lizard", + Languages: []string{"C", "CPP", "Java", "C#", "JavaScript", "TypeScript", "VueJS", "Objective-C", "Swift", "Python", "Ruby", "TTCN-3", "PHP", "Scala", "GDScript", "Golang", "Lua", "Rust", "Fortran", "Kotlin", "Solidity", "Erlang", "Zig", "Perl"}, + Extensions: []string{".c", ".cpp", ".cc", ".h", ".hpp", ".java", ".cs", ".js", ".jsx", ".ts", ".tsx", ".vue", ".m", ".swift", ".py", ".rb", ".ttcn", ".php", ".scala", ".gd", ".go", ".lua", ".rs", ".f", ".f90", ".kt", ".sol", ".erl", ".zig", ".pl"}, + }, + "semgrep": { + Name: "semgrep", + Languages: []string{"C", "CPP", "C#", "Generic", "Go", "Java", "JavaScript", "JSON", "Kotlin", "Python", "TypeScript", "Ruby", "Rust", "JSX", "PHP", "Scala", "Swift", "Terraform"}, + Extensions: []string{".c", ".cpp", ".h", ".hpp", ".cs", ".go", ".java", ".js", ".json", ".kt", ".py", ".ts", ".rb", ".rs", ".jsx", ".php", ".scala", ".swift", ".tf", ".tfvars"}, + }, } // Build a list of tool language info for enabled tools diff --git a/tools/pmdRunner_test.go b/tools/pmdRunner_test.go deleted file mode 100644 index 30bea013..00000000 --- a/tools/pmdRunner_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package tools - -import ( - "codacy/cli-v2/config" - "encoding/json" - "log" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -// SarifResult represents a single result in a SARIF report -type SarifResult struct { - RuleID string `json:"ruleId"` - Message struct { - Text string `json:"text"` - } `json:"message"` - Locations []struct { - PhysicalLocation struct { - ArtifactLocation struct { - URI string `json:"uri"` - } `json:"artifactLocation"` - Region struct { - StartLine int `json:"startLine"` - StartColumn int `json:"startColumn"` - EndLine int `json:"endLine"` - EndColumn int `json:"endColumn"` - } `json:"region"` - } `json:"physicalLocation"` - } `json:"locations"` -} - -// SarifReport represents the structure of a SARIF report -type SarifReport struct { - Runs []struct { - Results []SarifResult `json:"results"` - } `json:"runs"` -} - -func TestRunPmdToFile(t *testing.T) { - homeDirectory, err := os.UserHomeDir() - if err != nil { - log.Fatal(err.Error()) - } - currentDirectory, err := os.Getwd() - if err != nil { - log.Fatal(err.Error()) - } - - // Use the correct path relative to tools directory - testDirectory := filepath.Join(currentDirectory, "testdata", "repositories", "pmd") - repositoryCache := filepath.Join(testDirectory, ".codacy") - globalCache := filepath.Join(homeDirectory, ".cache/codacy") - - tempResultFile := filepath.Join(os.TempDir(), "pmd.sarif") - defer os.Remove(tempResultFile) - - config := *config.NewConfigType(testDirectory, repositoryCache, globalCache) - - // Use absolute paths - repositoryToAnalyze := testDirectory - // Use the standard ruleset file for testing the PMD runner functionality - //rulesetFile := filepath.Join(testDirectory, "ruleset.xml") - - // Use the same path as defined in plugin.yaml - pmdBinary := filepath.Join(globalCache, "tools/pmd@6.55.0/pmd-bin-6.55.0/bin/run.sh") - - // Run PMD - err = RunPmd(repositoryToAnalyze, pmdBinary, nil, tempResultFile, "sarif", config) - if err != nil { - t.Fatalf("Failed to run pmd: %v", err) - } - - // Check if the output file was created - obtainedSarifBytes, err := os.ReadFile(tempResultFile) - if err != nil { - t.Fatalf("Failed to read output file: %v", err) - } - - // Normalize paths in the obtained SARIF output - obtainedSarif := string(obtainedSarifBytes) - obtainedSarif = strings.ReplaceAll(obtainedSarif, currentDirectory+"/", "") - - // Parse the normalized SARIF output - var sarifReport SarifReport - err = json.Unmarshal([]byte(obtainedSarif), &sarifReport) - if err != nil { - t.Fatalf("Failed to parse SARIF output: %v", err) - } - - // Verify we have results - assert.NotEmpty(t, sarifReport.Runs, "SARIF report should have at least one run") - assert.NotEmpty(t, sarifReport.Runs[0].Results, "SARIF report should have at least one result") - - // Define expected violations - expectedViolations := map[string]bool{ - "UnusedPrivateField": false, - "ShortVariable": false, - "AtLeastOneConstructor": false, - "CommentRequired": false, - } - - // Check each result - for _, result := range sarifReport.Runs[0].Results { - // Mark this rule as found - expectedViolations[result.RuleID] = true - - // Verify the file path is correct - assert.Contains(t, result.Locations[0].PhysicalLocation.ArtifactLocation.URI, "RulesBreaker.java", - "Violation should be in RulesBreaker.java") - - // Verify line numbers are reasonable - assert.Greater(t, result.Locations[0].PhysicalLocation.Region.StartLine, 0, - "Start line should be positive") - assert.Less(t, result.Locations[0].PhysicalLocation.Region.StartLine, 30, - "Start line should be within the file") - } - - // Verify all expected violations were found - for ruleID, found := range expectedViolations { - assert.True(t, found, "Expected violation %s was not found", ruleID) - } -} diff --git a/tools/semgrepConfigCreator.go b/tools/semgrepConfigCreator.go index 6272bffe..44c051b6 100644 --- a/tools/semgrepConfigCreator.go +++ b/tools/semgrepConfigCreator.go @@ -15,6 +15,10 @@ type semgrepRulesFile struct { Rules []map[string]interface{} `yaml:"rules"` } +// getExecutablePath is a variable that holds the function to get the executable path +// This is used for testing purposes +var getExecutablePath = os.Executable + // FilterRulesFromFile extracts enabled rules from a rules.yaml file based on configuration func FilterRulesFromFile(rulesFilePath string, config []domain.PatternConfiguration) ([]byte, error) { // Read the rules.yaml file @@ -65,8 +69,15 @@ func FilterRulesFromFile(rulesFilePath string, config []domain.PatternConfigurat // GetSemgrepConfig gets the Semgrep configuration based on the pattern configuration func GetSemgrepConfig(config []domain.PatternConfiguration) ([]byte, error) { - // Get the default rules file location - rulesFile := filepath.Join("plugins", "tools", "semgrep", "rules.yaml") + // Get the executable's directory + execPath, err := getExecutablePath() + if err != nil { + return nil, fmt.Errorf("failed to get executable path: %w", err) + } + execDir := filepath.Dir(execPath) + + // Get the default rules file location relative to the executable + rulesFile := filepath.Join(execDir, "plugins", "tools", "semgrep", "rules.yaml") // Check if it exists and config is not empty if _, err := os.Stat(rulesFile); err == nil && len(config) > 0 { @@ -80,8 +91,15 @@ func GetSemgrepConfig(config []domain.PatternConfiguration) ([]byte, error) { // GetDefaultSemgrepConfig gets the default Semgrep configuration func GetDefaultSemgrepConfig() ([]byte, error) { - // Get the default rules file location - rulesFile := filepath.Join("plugins", "tools", "semgrep", "rules.yaml") + // Get the executable's directory + execPath, err := getExecutablePath() + if err != nil { + return nil, fmt.Errorf("failed to get executable path: %w", err) + } + execDir := filepath.Dir(execPath) + + // Get the default rules file location relative to the executable + rulesFile := filepath.Join(execDir, "plugins", "tools", "semgrep", "rules.yaml") // If the file exists, return its contents if _, err := os.Stat(rulesFile); err == nil { diff --git a/tools/semgrepConfigCreator_test.go b/tools/semgrepConfigCreator_test.go index b22cb54a..992fc413 100644 --- a/tools/semgrepConfigCreator_test.go +++ b/tools/semgrepConfigCreator_test.go @@ -101,41 +101,30 @@ func TestFilterRulesFromFile(t *testing.T) { // TestGetSemgrepConfig tests the GetSemgrepConfig function func TestGetSemgrepConfig(t *testing.T) { - // Override the function to find rules.yaml to use our test file - originalRulesFilePath := filepath.Join("plugins", "tools", "semgrep", "rules.yaml") - // Create a temporary rules file tempDir := t.TempDir() testRulesFile := filepath.Join(tempDir, "rules.yaml") err := os.WriteFile(testRulesFile, []byte(sampleRulesYAML), 0644) assert.NoError(t, err) - // Create a backup of the original file if it exists - backupFilePath := "" - if _, err := os.Stat(originalRulesFilePath); err == nil { - backupFilePath = originalRulesFilePath + ".bak" - err = os.Rename(originalRulesFilePath, backupFilePath) - assert.NoError(t, err) + // Create a mock executable path that points to our temp directory + originalGetExecutablePath := getExecutablePath + getExecutablePath = func() (string, error) { + return filepath.Join(tempDir, "test-executable"), nil } + defer func() { + getExecutablePath = originalGetExecutablePath + }() - // Ensure the directory exists - err = os.MkdirAll(filepath.Dir(originalRulesFilePath), 0755) + // Create the plugins directory structure + pluginsDir := filepath.Join(tempDir, "plugins", "tools", "semgrep") + err = os.MkdirAll(pluginsDir, 0755) assert.NoError(t, err) - // Copy our test file to the location - testFileContent, err := os.ReadFile(testRulesFile) - assert.NoError(t, err) - err = os.WriteFile(originalRulesFilePath, testFileContent, 0644) + // Copy our test file to the plugins directory + err = os.WriteFile(filepath.Join(pluginsDir, "rules.yaml"), []byte(sampleRulesYAML), 0644) assert.NoError(t, err) - // Clean up after the test - defer func() { - os.Remove(originalRulesFilePath) - if backupFilePath != "" { - os.Rename(backupFilePath, originalRulesFilePath) - } - }() - // Test with valid configuration config := []domain.PatternConfiguration{ { @@ -162,41 +151,30 @@ func TestGetSemgrepConfig(t *testing.T) { // TestGetDefaultSemgrepConfig tests the GetDefaultSemgrepConfig function func TestGetDefaultSemgrepConfig(t *testing.T) { - // Override the function to find rules.yaml to use our test file - originalRulesFilePath := filepath.Join("plugins", "tools", "semgrep", "rules.yaml") - // Create a temporary rules file tempDir := t.TempDir() testRulesFile := filepath.Join(tempDir, "rules.yaml") err := os.WriteFile(testRulesFile, []byte(sampleRulesYAML), 0644) assert.NoError(t, err) - // Create a backup of the original file if it exists - backupFilePath := "" - if _, err := os.Stat(originalRulesFilePath); err == nil { - backupFilePath = originalRulesFilePath + ".bak" - err = os.Rename(originalRulesFilePath, backupFilePath) - assert.NoError(t, err) + // Create a mock executable path that points to our temp directory + originalGetExecutablePath := getExecutablePath + getExecutablePath = func() (string, error) { + return filepath.Join(tempDir, "test-executable"), nil } + defer func() { + getExecutablePath = originalGetExecutablePath + }() - // Ensure the directory exists - err = os.MkdirAll(filepath.Dir(originalRulesFilePath), 0755) + // Create the plugins directory structure + pluginsDir := filepath.Join(tempDir, "plugins", "tools", "semgrep") + err = os.MkdirAll(pluginsDir, 0755) assert.NoError(t, err) - // Copy our test file to the location - testFileContent, err := os.ReadFile(testRulesFile) - assert.NoError(t, err) - err = os.WriteFile(originalRulesFilePath, testFileContent, 0644) + // Copy our test file to the plugins directory + err = os.WriteFile(filepath.Join(pluginsDir, "rules.yaml"), []byte(sampleRulesYAML), 0644) assert.NoError(t, err) - // Clean up after the test - defer func() { - os.Remove(originalRulesFilePath) - if backupFilePath != "" { - os.Rename(backupFilePath, originalRulesFilePath) - } - }() - // Test getting default config result, err := GetDefaultSemgrepConfig() assert.NoError(t, err) @@ -207,7 +185,7 @@ func TestGetDefaultSemgrepConfig(t *testing.T) { assert.Equal(t, 3, len(parsedRules.Rules)) // Test when rules.yaml doesn't exist - os.Remove(originalRulesFilePath) + os.Remove(filepath.Join(pluginsDir, "rules.yaml")) _, err = GetDefaultSemgrepConfig() assert.Error(t, err) assert.Contains(t, err.Error(), "rules.yaml not found") diff --git a/tools/semgrepRunner_test.go b/tools/semgrepRunner_test.go deleted file mode 100644 index 1d81344f..00000000 --- a/tools/semgrepRunner_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package tools - -import ( - "codacy/cli-v2/plugins" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRunSemgrepWithSpecificFiles(t *testing.T) { - homeDirectory, err := os.UserHomeDir() - if err != nil { - t.Fatalf("Failed to get home directory: %v", err) - } - currentDirectory, err := os.Getwd() - if err != nil { - t.Fatalf("Failed to get current directory: %v", err) - } - - // Set up test directories and files - testDirectory := filepath.Join(currentDirectory, "testdata", "repositories", "semgrep") - tempResultFile := filepath.Join(os.TempDir(), "semgrep-specific.sarif") - defer os.Remove(tempResultFile) - - // Create tool info for semgrep - toolInfo := &plugins.ToolInfo{ - Binaries: map[string]string{ - "semgrep": filepath.Join(homeDirectory, ".cache/codacy/tools/semgrep@1.78.0/venv/bin/semgrep"), - }, - } - - // Specify files to analyze - filesToAnalyze := []string{"sample.js"} - - // Run Semgrep analysis on specific files - err = RunSemgrep(testDirectory, toolInfo.Binaries["semgrep"], filesToAnalyze, tempResultFile, "sarif") - if err != nil { - t.Fatalf("Failed to run semgrep on specific files: %v", err) - } - - // Verify file exists and has content - fileInfo, err := os.Stat(tempResultFile) - assert.NoError(t, err, "Failed to stat output file") - assert.Greater(t, fileInfo.Size(), int64(0), "Output file should not be empty") -} diff --git a/tools/trivyRunner_test.go b/tools/trivyRunner_test.go deleted file mode 100644 index c7d1dd66..00000000 --- a/tools/trivyRunner_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package tools - -import ( - "fmt" - "log" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRunTrivyToFile(t *testing.T) { - homeDirectory, err := os.UserHomeDir() - if err != nil { - log.Fatal(err.Error()) - } - currentDirectory, err := os.Getwd() - if err != nil { - log.Fatal(err.Error()) - } - - testDirectory := "testdata/repositories/trivy" - tempResultFile := filepath.Join(os.TempDir(), "trivy.sarif") - defer os.Remove(tempResultFile) - - repositoryToAnalyze := filepath.Join(testDirectory, "src") - - trivyBinary := filepath.Join(homeDirectory, ".cache/codacy/tools/trivy@0.59.1/trivy") - - err = RunTrivy(repositoryToAnalyze, trivyBinary, nil, tempResultFile, "sarif") - if err != nil { - t.Fatalf("Failed to run trivy: %v", err) - } - - // Check if the output file was created - obtainedSarifBytes, err := os.ReadFile(tempResultFile) - if err != nil { - t.Fatalf("Failed to read output file: %v", err) - } - obtainedSarif := string(obtainedSarifBytes) - filePrefix := "file://" + currentDirectory + "/" - fmt.Println(filePrefix) - actualSarif := strings.ReplaceAll(obtainedSarif, filePrefix, "") - - // Read the expected SARIF - expectedSarifFile := filepath.Join(testDirectory, "expected.sarif") - expectedSarifBytes, err := os.ReadFile(expectedSarifFile) - if err != nil { - log.Fatal(err) - } - expectedSarif := strings.TrimSpace(string(expectedSarifBytes)) - - assert.Equal(t, expectedSarif, actualSarif, "output did not match expected") -}