diff --git a/.docker/docker-entrypoint.sh b/.docker/docker-entrypoint.sh index cf794707e..e660a6ee1 100755 --- a/.docker/docker-entrypoint.sh +++ b/.docker/docker-entrypoint.sh @@ -310,10 +310,11 @@ ACQUIS_EOF echo "✗ WARNING: LAPI port configuration may be incorrect" fi - # Update hub index to ensure CrowdSec can start - if [ ! -f "/etc/crowdsec/hub/.index.json" ]; then - echo "Updating CrowdSec hub index..." - timeout 60s cscli hub update 2>/dev/null || echo "⚠️ Hub update timed out or failed, continuing..." + # Always refresh hub index on startup (stale index causes hash mismatch errors on collection install) + echo "Updating CrowdSec hub index..." + if ! timeout 60s cscli hub update 2>&1; then + echo "⚠️ Hub index update failed (network issue?). Collections may fail to install." + echo " CrowdSec will still start with whatever index is cached." fi # Ensure local machine is registered (auto-heal for volume/config mismatch) @@ -321,12 +322,11 @@ ACQUIS_EOF echo "Registering local machine..." cscli machines add -a --force 2>/dev/null || echo "Warning: Machine registration may have failed" - # Install hub items (parsers, scenarios, collections) if local mode enabled - if [ "$SECURITY_CROWDSEC_MODE" = "local" ]; then - echo "Installing CrowdSec hub items..." - if [ -x /usr/local/bin/install_hub_items.sh ]; then - /usr/local/bin/install_hub_items.sh 2>/dev/null || echo "Warning: Some hub items may not have installed" - fi + # Always ensure required collections are present (idempotent — already-installed items are skipped). + # Collections are just config files with zero runtime cost when CrowdSec is disabled. + echo "Ensuring CrowdSec hub items are installed..." + if [ -x /usr/local/bin/install_hub_items.sh ]; then + /usr/local/bin/install_hub_items.sh || echo "⚠️ Some hub items may not have installed. CrowdSec can still start." fi # Fix ownership AFTER cscli commands (they run as root and create root-owned files) diff --git a/.github/agents/Management.agent.md b/.github/agents/Management.agent.md index 9d1be6570..d01d687d6 100644 --- a/.github/agents/Management.agent.md +++ b/.github/agents/Management.agent.md @@ -167,23 +167,27 @@ The task is not complete until ALL of the following pass with zero issues: - **Base URL**: Uses `PLAYWRIGHT_BASE_URL` or default from `playwright.config.js` - All E2E tests must pass before proceeding to unit tests -2. **Local Patch Coverage Preflight (MANDATORY - Before Unit/Coverage Tests)**: - - Ensure the local patch report is run first via VS Code task `Test: Local Patch Report` or `bash scripts/local-patch-report.sh`. - - Verify both artifacts exist: `test-results/local-patch-report.md` and `test-results/local-patch-report.json`. - - Use this report to identify changed files needing coverage before running backend/frontend coverage suites. - -3. **Coverage Tests (MANDATORY - Verify Explicitly)**: +2. **Coverage Tests (MANDATORY - Verify Explicitly)**: - **Backend**: Ensure `Backend_Dev` ran VS Code task "Test: Backend with Coverage" or `scripts/go-test-coverage.sh` - **Frontend**: Ensure `Frontend_Dev` ran VS Code task "Test: Frontend with Coverage" or `scripts/frontend-test-coverage.sh` - **Why**: These are in manual stage of pre-commit for performance. Subagents MUST run them via VS Code tasks or scripts. - Minimum coverage: 85% for both backend and frontend. - All tests must pass with zero failures. + - **Outputs**: `backend/coverage.txt` and `frontend/coverage/lcov.info` — these are required inputs for step 3. + +3. **Local Patch Coverage Report (MANDATORY - After Coverage Tests)**: + - **Purpose**: Identify uncovered lines in files modified by this task so missing tests are written before declaring Done. This is the bridge between "overall coverage is fine" and "the actual lines I changed are tested." + - **Prerequisites**: `backend/coverage.txt` and `frontend/coverage/lcov.info` must exist (generated by step 2). If missing, run coverage tests first. + - **Run**: VS Code task `Test: Local Patch Report` or `bash scripts/local-patch-report.sh`. + - **Verify artifacts**: Both `test-results/local-patch-report.md` and `test-results/local-patch-report.json` must exist with non-empty results. + - **Act on findings**: If patch coverage for any changed file is below **90%**, delegate to the responsible agent (`Backend_Dev` or `Frontend_Dev`) to add targeted tests covering the uncovered lines. Re-run coverage (step 2) and this report until the threshold is met. + - **Blocking gate**: 90% overall patch coverage. Do not proceed to pre-commit or security scans until resolved or explicitly waived by the user. 4. **Type Safety (Frontend)**: - Ensure `Frontend_Dev` ran VS Code task "Lint: TypeScript Check" or `npm run type-check` - **Why**: This check is in manual stage of pre-commit for performance. Subagents MUST run it explicitly. -5. **Pre-commit Hooks**: Ensure `QA_Security` ran `pre-commit run --all-files` (fast hooks only; coverage was verified in step 3) +5. **Pre-commit Hooks**: Ensure `QA_Security` ran `pre-commit run --all-files` (fast hooks only; coverage was verified in step 2) 6. **Security Scans**: Ensure `QA_Security` ran the following with zero Critical or High severity issues: - **Trivy Filesystem Scan**: Fast scan of source code and dependencies diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md index 9878a0d48..7a9dde5e6 100644 --- a/.github/instructions/testing.instructions.md +++ b/.github/instructions/testing.instructions.md @@ -12,9 +12,19 @@ instruction files take precedence over agent files and operator documentation. **MANDATORY**: Before running unit tests, verify the application UI/UX functions correctly end-to-end. -## 0.5 Local Patch Coverage Preflight (Before Unit Tests) +## 0.5 Local Patch Coverage Report (After Coverage Tests) -**MANDATORY**: After E2E and before backend/frontend unit coverage runs, generate a local patch report so uncovered changed lines are visible early. +**MANDATORY**: After running backend and frontend coverage tests (which generate +`backend/coverage.txt` and `frontend/coverage/lcov.info`), run the local patch +report to identify uncovered lines in changed files. + +**Purpose**: Overall coverage can be healthy while the specific lines you changed +are untested. This step catches that gap. If uncovered lines are found in +feature code, add targeted tests before completing the task. + +**Prerequisites**: Coverage artifacts must exist before running the report: +- `backend/coverage.txt` — generated by `scripts/go-test-coverage.sh` +- `frontend/coverage/lcov.info` — generated by `scripts/frontend-test-coverage.sh` Run one of the following from `/projects/Charon`: @@ -26,11 +36,14 @@ Test: Local Patch Report bash scripts/local-patch-report.sh ``` -Required artifacts: +Required output artifacts: - `test-results/local-patch-report.md` - `test-results/local-patch-report.json` -This preflight is advisory for thresholds during rollout, but artifact generation is required in DoD. +**Action on results**: If patch coverage for any changed file is below 90%, add +tests targeting the uncovered changed lines. Re-run coverage and this report to +verify improvement. Artifact generation is required for DoD regardless of +threshold results. ### PREREQUISITE: Start E2E Environment diff --git a/.github/renovate.json b/.github/renovate.json index 45a9a1e81..a386fb539 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -11,6 +11,7 @@ "development" ], + "postUpdateOptions": ["npmDedupe"], "timezone": "America/New_York", "dependencyDashboard": true, "dependencyDashboardApproval": true, diff --git a/.github/skills/security-scan-docker-image-scripts/run.sh b/.github/skills/security-scan-docker-image-scripts/run.sh index c77204dd1..241764f0e 100755 --- a/.github/skills/security-scan-docker-image-scripts/run.sh +++ b/.github/skills/security-scan-docker-image-scripts/run.sh @@ -139,7 +139,10 @@ log_info "This may take 30-60 seconds on first run (database download)" # Run Grype against the SBOM (generated from image, not filesystem) # This matches exactly what CI does in supply-chain-pr.yml +# --config ensures .grype.yaml ignore rules are applied, separating +# ignored matches from actionable ones in the JSON output if grype sbom:sbom.cyclonedx.json \ + --config .grype.yaml \ --output json \ --file grype-results.json; then log_success "Vulnerability scan complete" @@ -149,6 +152,7 @@ fi # Generate SARIF output for GitHub Security (matches CI) grype sbom:sbom.cyclonedx.json \ + --config .grype.yaml \ --output sarif \ --file grype-results.sarif 2>/dev/null || true diff --git a/.github/workflows/auto-add-to-project.yml b/.github/workflows/auto-add-to-project.yml index 658beadca..fa74c1464 100644 --- a/.github/workflows/auto-add-to-project.yml +++ b/.github/workflows/auto-add-to-project.yml @@ -8,6 +8,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }} cancel-in-progress: false +permissions: + contents: read + jobs: add-to-project: runs-on: ubuntu-latest diff --git a/.github/workflows/auto-changelog.yml b/.github/workflows/auto-changelog.yml index 2c70e8b37..b5de096c9 100644 --- a/.github/workflows/auto-changelog.yml +++ b/.github/workflows/auto-changelog.yml @@ -12,6 +12,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} cancel-in-progress: true +permissions: + contents: write + jobs: update-draft: runs-on: ubuntu-latest diff --git a/.github/workflows/auto-label-issues.yml b/.github/workflows/auto-label-issues.yml index fcae6b7b8..40b466a4a 100644 --- a/.github/workflows/auto-label-issues.yml +++ b/.github/workflows/auto-label-issues.yml @@ -8,6 +8,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.issue.number }} cancel-in-progress: true +permissions: + contents: read + jobs: auto-label: runs-on: ubuntu-latest diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 2ba2e465e..f22535cbe 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -35,7 +35,7 @@ jobs: ref: ${{ github.event.workflow_run.head_sha || github.sha }} - name: Set up Go - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/cerberus-integration.yml b/.github/workflows/cerberus-integration.yml index e43474a2d..5791922e9 100644 --- a/.github/workflows/cerberus-integration.yml +++ b/.github/workflows/cerberus-integration.yml @@ -20,6 +20,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.workflow_run.event || github.event_name }}-${{ github.event.workflow_run.head_branch || github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: cerberus-integration: name: Cerberus Security Stack Integration diff --git a/.github/workflows/codecov-upload.yml b/.github/workflows/codecov-upload.yml index 0e2aaec7f..6eaa31b23 100644 --- a/.github/workflows/codecov-upload.yml +++ b/.github/workflows/codecov-upload.yml @@ -45,7 +45,7 @@ jobs: ref: ${{ github.sha }} - name: Set up Go - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: go-version: ${{ env.GO_VERSION }} @@ -146,7 +146,7 @@ jobs: retention-days: 7 - name: Upload backend coverage to Codecov - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./backend/coverage.txt @@ -183,7 +183,7 @@ jobs: exit "${PIPESTATUS[0]}" - name: Upload frontend coverage to Codecov - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6 with: token: ${{ secrets.CODECOV_TOKEN }} directory: ./frontend/coverage diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d67d6c6b0..e6b563e94 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -52,7 +52,7 @@ jobs: run: bash scripts/ci/check-codeql-parity.sh - name: Initialize CodeQL - uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4 + uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4 with: languages: ${{ matrix.language }} queries: security-and-quality @@ -63,7 +63,7 @@ jobs: - name: Setup Go if: matrix.language == 'go' - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: go-version: ${{ env.GO_VERSION }} cache-dependency-path: backend/go.sum @@ -92,10 +92,10 @@ jobs: run: mkdir -p sarif-results - name: Autobuild - uses: github/codeql-action/autobuild@38697555549f1db7851b81482ff19f1fa5c4fedc # v4 + uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4 + uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4 with: category: "/language:${{ matrix.language }}" output: sarif-results/${{ matrix.language }} diff --git a/.github/workflows/create-labels.yml b/.github/workflows/create-labels.yml index 284d3efb0..4c607de52 100644 --- a/.github/workflows/create-labels.yml +++ b/.github/workflows/create-labels.yml @@ -8,6 +8,9 @@ concurrency: group: ${{ github.workflow }} cancel-in-progress: false +permissions: + contents: read + jobs: create-labels: runs-on: ubuntu-latest diff --git a/.github/workflows/crowdsec-integration.yml b/.github/workflows/crowdsec-integration.yml index 868d8e94e..28e582c0d 100644 --- a/.github/workflows/crowdsec-integration.yml +++ b/.github/workflows/crowdsec-integration.yml @@ -20,6 +20,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.workflow_run.event || github.event_name }}-${{ github.event.workflow_run.head_branch || github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: crowdsec-integration: name: CrowdSec Bouncer Integration diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 9029ac243..57a222ddb 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -33,6 +33,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} cancel-in-progress: true +permissions: + contents: read + env: GHCR_REGISTRY: ghcr.io DOCKERHUB_REGISTRY: docker.io @@ -130,7 +133,7 @@ jobs: - name: Log in to GitHub Container Registry if: steps.skip.outputs.skip_build != 'true' - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.GHCR_REGISTRY }} username: ${{ github.actor }} @@ -138,7 +141,7 @@ jobs: - name: Log in to Docker Hub if: steps.skip.outputs.skip_build != 'true' && env.HAS_DOCKERHUB_TOKEN == 'true' - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: docker.io username: ${{ secrets.DOCKERHUB_USERNAME }} @@ -565,7 +568,7 @@ jobs: - name: Upload Trivy results if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.trivy-check.outputs.exists == 'true' - uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: sarif_file: 'trivy-results.sarif' category: '.github/workflows/docker-build.yml:build-and-push' @@ -594,7 +597,7 @@ jobs: # Install Cosign for keyless signing - name: Install Cosign if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true' - uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0 + uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 # Sign GHCR image with keyless signing (Sigstore/Fulcio) - name: Sign GHCR Image @@ -660,7 +663,7 @@ jobs: echo "image_ref=${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${PR_TAG}" >> "$GITHUB_OUTPUT" - name: Log in to GitHub Container Registry - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.GHCR_REGISTRY }} username: ${{ github.actor }} @@ -724,14 +727,14 @@ jobs: - name: Upload Trivy scan results if: always() && steps.trivy-pr-check.outputs.exists == 'true' - uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: sarif_file: 'trivy-pr-results.sarif' category: 'docker-pr-image' - name: Upload Trivy compatibility results (docker-build category) if: always() && steps.trivy-pr-check.outputs.exists == 'true' - uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: sarif_file: 'trivy-pr-results.sarif' category: '.github/workflows/docker-build.yml:build-and-push' @@ -739,7 +742,7 @@ jobs: - name: Upload Trivy compatibility results (docker-publish alias) if: always() && steps.trivy-pr-check.outputs.exists == 'true' - uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: sarif_file: 'trivy-pr-results.sarif' category: '.github/workflows/docker-publish.yml:build-and-push' @@ -747,7 +750,7 @@ jobs: - name: Upload Trivy compatibility results (nightly alias) if: always() && steps.trivy-pr-check.outputs.exists == 'true' - uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: sarif_file: 'trivy-pr-results.sarif' category: 'trivy-nightly' diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index eda97942b..82a2dc900 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -372,7 +372,7 @@ jobs: # Deploy to GitHub Pages - name: 🚀 Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5 # Create a summary - name: 📋 Create deployment summary diff --git a/.github/workflows/e2e-tests-split.yml b/.github/workflows/e2e-tests-split.yml index ed20bfebe..2abde8340 100644 --- a/.github/workflows/e2e-tests-split.yml +++ b/.github/workflows/e2e-tests-split.yml @@ -142,7 +142,7 @@ jobs: - name: Set up Go if: steps.resolve-image.outputs.image_source == 'build' - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: go-version: ${{ env.GO_VERSION }} @@ -233,7 +233,7 @@ jobs: - name: Log in to Docker Hub if: needs.build.outputs.image_source == 'registry' - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.DOCKERHUB_REGISTRY }} username: ${{ secrets.DOCKERHUB_USERNAME }} @@ -435,7 +435,7 @@ jobs: - name: Log in to Docker Hub if: needs.build.outputs.image_source == 'registry' - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.DOCKERHUB_REGISTRY }} username: ${{ secrets.DOCKERHUB_USERNAME }} @@ -645,7 +645,7 @@ jobs: - name: Log in to Docker Hub if: needs.build.outputs.image_source == 'registry' - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.DOCKERHUB_REGISTRY }} username: ${{ secrets.DOCKERHUB_USERNAME }} @@ -899,7 +899,7 @@ jobs: - name: Log in to Docker Hub if: needs.build.outputs.image_source == 'registry' - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.DOCKERHUB_REGISTRY }} username: ${{ secrets.DOCKERHUB_USERNAME }} @@ -1136,7 +1136,7 @@ jobs: - name: Log in to Docker Hub if: needs.build.outputs.image_source == 'registry' - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.DOCKERHUB_REGISTRY }} username: ${{ secrets.DOCKERHUB_USERNAME }} @@ -1381,7 +1381,7 @@ jobs: - name: Log in to Docker Hub if: needs.build.outputs.image_source == 'registry' - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.DOCKERHUB_REGISTRY }} username: ${{ secrets.DOCKERHUB_USERNAME }} diff --git a/.github/workflows/gh_cache_cleanup.yml b/.github/workflows/gh_cache_cleanup.yml index dde5a6525..020c7b557 100644 --- a/.github/workflows/gh_cache_cleanup.yml +++ b/.github/workflows/gh_cache_cleanup.yml @@ -7,6 +7,9 @@ on: required: true type: string +permissions: + contents: read + jobs: cleanup: runs-on: ubuntu-latest diff --git a/.github/workflows/history-rewrite-tests.yml b/.github/workflows/history-rewrite-tests.yml index ceca9d97e..697e2d831 100644 --- a/.github/workflows/history-rewrite-tests.yml +++ b/.github/workflows/history-rewrite-tests.yml @@ -9,6 +9,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} cancel-in-progress: true +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 44f8e8965..326c1ed17 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -22,6 +22,9 @@ env: DOCKERHUB_REGISTRY: docker.io IMAGE_NAME: wikid82/charon +permissions: + contents: read + jobs: sync-development-to-nightly: runs-on: ubuntu-latest @@ -178,7 +181,7 @@ jobs: echo "image=${ALPINE_IMAGE_REF}" >> "$GITHUB_OUTPUT" - name: Log in to GitHub Container Registry - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.GHCR_REGISTRY }} username: ${{ github.actor }} @@ -186,7 +189,7 @@ jobs: - name: Log in to Docker Hub if: env.HAS_DOCKERHUB_TOKEN == 'true' - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: docker.io username: ${{ secrets.DOCKERHUB_USERNAME }} @@ -333,7 +336,7 @@ jobs: # Install Cosign for keyless signing - name: Install Cosign - uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0 + uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 # Sign GHCR image with keyless signing (Sigstore/Fulcio) - name: Sign GHCR Image @@ -375,7 +378,7 @@ jobs: run: echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >> "$GITHUB_ENV" - name: Log in to GitHub Container Registry - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.GHCR_REGISTRY }} username: ${{ github.actor }} @@ -451,7 +454,7 @@ jobs: trivyignores: '.trivyignore' - name: Upload Trivy results - uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: sarif_file: 'trivy-nightly.sarif' category: 'trivy-nightly' diff --git a/.github/workflows/pr-checklist.yml b/.github/workflows/pr-checklist.yml index 188841bc5..4bf4f725f 100644 --- a/.github/workflows/pr-checklist.yml +++ b/.github/workflows/pr-checklist.yml @@ -12,6 +12,10 @@ concurrency: group: ${{ github.workflow }}-${{ inputs.pr_number || github.event.pull_request.number }} cancel-in-progress: true +permissions: + contents: read + pull-requests: write + jobs: validate: name: Validate history-rewrite checklist (conditional) diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index d14dec74a..e52013d68 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -31,7 +31,7 @@ jobs: ref: ${{ github.sha }} - name: Set up Go - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ env.GO_VERSION }} @@ -138,7 +138,7 @@ jobs: } >> "$GITHUB_ENV" - name: Set up Go - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ env.GO_VERSION }} @@ -268,6 +268,12 @@ jobs: cache: 'npm' cache-dependency-path: frontend/package-lock.json + - name: Verify lockfile integrity and audit dependencies + working-directory: frontend + run: | + npm ci --ignore-scripts + npm audit --audit-level=critical + - name: Check if frontend was modified in PR id: check-frontend run: | diff --git a/.github/workflows/rate-limit-integration.yml b/.github/workflows/rate-limit-integration.yml index fe49d0f67..bdaca5438 100644 --- a/.github/workflows/rate-limit-integration.yml +++ b/.github/workflows/rate-limit-integration.yml @@ -20,6 +20,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.workflow_run.event || github.event_name }}-${{ github.event.workflow_run.head_branch || github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: rate-limit-integration: name: Rate Limiting Integration diff --git a/.github/workflows/release-goreleaser.yml b/.github/workflows/release-goreleaser.yml index 81988901b..f2ab354c8 100644 --- a/.github/workflows/release-goreleaser.yml +++ b/.github/workflows/release-goreleaser.yml @@ -45,7 +45,7 @@ jobs: fi - name: Set up Go - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml index 82e1bec5a..54d963d1d 100644 --- a/.github/workflows/renovate.yml +++ b/.github/workflows/renovate.yml @@ -25,7 +25,7 @@ jobs: fetch-depth: 1 - name: Run Renovate - uses: renovatebot/github-action@68a3ea99af6ad249940b5a9fdf44fc6d7f14378b # v46.1.6 + uses: renovatebot/github-action@3633cede7d4d4598438e654eac4a695e46004420 # v46.1.7 with: configurationFile: .github/renovate.json token: ${{ secrets.RENOVATE_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/repo-health.yml b/.github/workflows/repo-health.yml index 6c11cec3c..220d2f699 100644 --- a/.github/workflows/repo-health.yml +++ b/.github/workflows/repo-health.yml @@ -9,6 +9,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }} cancel-in-progress: true +permissions: + contents: read + jobs: repo_health: name: Repo health diff --git a/.github/workflows/security-pr.yml b/.github/workflows/security-pr.yml index 7e05d9de5..5f1491385 100644 --- a/.github/workflows/security-pr.yml +++ b/.github/workflows/security-pr.yml @@ -22,6 +22,9 @@ concurrency: group: security-pr-${{ github.event_name == 'workflow_run' && github.event.workflow_run.event || github.event_name }}-${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: security-scan: name: Trivy Binary Scan @@ -361,7 +364,7 @@ jobs: - name: Run Trivy filesystem scan (SARIF output) if: steps.check-artifact.outputs.artifact_exists == 'true' || github.event_name == 'push' || github.event_name == 'pull_request' - # aquasecurity/trivy-action v0.33.1 + # aquasecurity/trivy-action 0.35.0 uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 with: scan-type: 'fs' @@ -385,7 +388,7 @@ jobs: - name: Upload Trivy SARIF to GitHub Security if: always() && steps.trivy-sarif-check.outputs.exists == 'true' # github/codeql-action v4 - uses: github/codeql-action/upload-sarif@eedab83377f873ae39009d167a89b7a5aab4638b + uses: github/codeql-action/upload-sarif@34950e1b113b30df4edee1a6d3a605242df0c40b with: sarif_file: 'trivy-binary-results.sarif' category: ${{ steps.pr-info.outputs.is_push == 'true' && format('security-scan-{0}', github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.ref_name) || format('security-scan-pr-{0}', steps.pr-info.outputs.pr_number) }} @@ -393,7 +396,7 @@ jobs: - name: Run Trivy filesystem scan (fail on CRITICAL/HIGH) if: steps.check-artifact.outputs.artifact_exists == 'true' || github.event_name == 'push' || github.event_name == 'pull_request' - # aquasecurity/trivy-action v0.33.1 + # aquasecurity/trivy-action 0.35.0 uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 with: scan-type: 'fs' diff --git a/.github/workflows/security-weekly-rebuild.yml b/.github/workflows/security-weekly-rebuild.yml index 1efc9f40c..77c546787 100644 --- a/.github/workflows/security-weekly-rebuild.yml +++ b/.github/workflows/security-weekly-rebuild.yml @@ -19,6 +19,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false +permissions: + contents: read + env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository_owner }}/charon @@ -61,7 +64,7 @@ jobs: echo "Base image digest: $DIGEST" - name: Log in to Container Registry - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -113,7 +116,7 @@ jobs: version: 'v0.69.3' - name: Upload Trivy results to GitHub Security - uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: sarif_file: 'trivy-weekly-results.sarif' diff --git a/.github/workflows/supply-chain-pr.yml b/.github/workflows/supply-chain-pr.yml index 24775acb2..efa049fc5 100644 --- a/.github/workflows/supply-chain-pr.yml +++ b/.github/workflows/supply-chain-pr.yml @@ -281,7 +281,7 @@ jobs: echo "component_count=${COMPONENT_COUNT}" >> "$GITHUB_OUTPUT" echo "✅ SBOM generated with ${COMPONENT_COUNT} components" - # Scan for vulnerabilities using manual Grype installation (pinned to v0.107.1) + # Scan for vulnerabilities using manual Grype installation (pinned to v0.110.0) - name: Install Grype if: steps.set-target.outputs.image_name != '' run: | @@ -292,8 +292,8 @@ jobs: id: grype-scan run: | echo "🔍 Scanning SBOM for vulnerabilities..." - grype sbom:sbom.cyclonedx.json -o json > grype-results.json - grype sbom:sbom.cyclonedx.json -o sarif > grype-results.sarif + grype sbom:sbom.cyclonedx.json --config .grype.yaml -o json > grype-results.json + grype sbom:sbom.cyclonedx.json --config .grype.yaml -o sarif > grype-results.sarif - name: Debug Output Files if: steps.set-target.outputs.image_name != '' @@ -362,7 +362,7 @@ jobs: - name: Upload SARIF to GitHub Security if: steps.check-artifact.outputs.artifact_found == 'true' - uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4 + uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4 continue-on-error: true with: sarif_file: grype-results.sarif diff --git a/.github/workflows/waf-integration.yml b/.github/workflows/waf-integration.yml index 509eb5eea..76bc74d36 100644 --- a/.github/workflows/waf-integration.yml +++ b/.github/workflows/waf-integration.yml @@ -20,6 +20,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.workflow_run.event || github.event_name }}-${{ github.event.workflow_run.head_branch || github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: waf-integration: name: Coraza WAF Integration diff --git a/.grype.yaml b/.grype.yaml index 945b8297b..f04e59b42 100644 --- a/.grype.yaml +++ b/.grype.yaml @@ -32,7 +32,8 @@ ignore: # # Review: # - Reviewed 2026-03-18 (initial suppression): no upstream fix available. Set 30-day review. - # - Next review: 2026-04-18. Remove suppression immediately once upstream fixes. + # - Extended 2026-04-04: Alpine 3.23 still ships 3.5.5-r0. No upstream fix available. + # - Next review: 2026-05-18. Remove suppression immediately once upstream fixes. # # Removal Criteria: # - Alpine publishes a patched version of libcrypto3 and libssl3 @@ -52,7 +53,7 @@ ignore: No upstream fix: Alpine 3.23 still ships libcrypto3 3.5.5-r0 as of 2026-03-18. Charon terminates TLS at the Caddy layer; the Go backend does not act as a raw TLS 1.3 server. Risk accepted pending Alpine upstream patch. - expiry: "2026-04-18" # Initial 30-day review period. Extend in 14–30 day increments with documented justification. + expiry: "2026-05-18" # Extended 2026-04-04: Alpine 3.23 still ships 3.5.5-r0. Next review 2026-05-18. # Action items when this suppression expires: # 1. Check Alpine security tracker: https://security.alpinelinux.org/vuln/CVE-2026-2673 @@ -74,7 +75,7 @@ ignore: No upstream fix: Alpine 3.23 still ships libssl3 3.5.5-r0 as of 2026-03-18. Charon terminates TLS at the Caddy layer; the Go backend does not act as a raw TLS 1.3 server. Risk accepted pending Alpine upstream patch. - expiry: "2026-04-18" # Initial 30-day review period. See libcrypto3 entry above for action items. + expiry: "2026-05-18" # Extended 2026-04-04: see libcrypto3 entry above for action items. # GHSA-6g7g-w4f8-9c9x: buger/jsonparser Delete panic on malformed JSON (DoS) # Severity: HIGH (CVSS 7.5) @@ -105,7 +106,8 @@ ignore: # # Review: # - Reviewed 2026-03-19 (initial suppression): no upstream fix exists. Set 30-day review. - # - Next review: 2026-04-19. Remove suppression once buger/jsonparser ships a fix and + # - Extended 2026-04-04: no upstream fix available. buger/jsonparser issue #275 still open. + # - Next review: 2026-05-19. Remove suppression once buger/jsonparser ships a fix and # CrowdSec updates their dependency. # # Removal Criteria: @@ -130,7 +132,7 @@ ignore: Charon does not use this package directly; the vector requires reaching CrowdSec's internal JSON processing pipeline. Risk accepted; no remediation path until upstream ships a fix. Reviewed 2026-03-19: no patched release available. - expiry: "2026-04-19" # 30-day review: no fix exists. Extend in 30-day increments with documented justification. + expiry: "2026-05-19" # Extended 2026-04-04: no upstream fix. Next review 2026-05-19. # Action items when this suppression expires: # 1. Check buger/jsonparser releases: https://github.com/buger/jsonparser/releases @@ -174,7 +176,8 @@ ignore: # Review: # - Reviewed 2026-03-19 (initial suppression): pgproto3/v2 is EOL; no fix exists or will exist. # Waiting on CrowdSec to migrate to pgx/v5. Set 30-day review. - # - Next review: 2026-04-19. Remove suppression once CrowdSec ships with pgx/v5. + # - Extended 2026-04-04: CrowdSec has not migrated to pgx/v5 yet. + # - Next review: 2026-05-19. Remove suppression once CrowdSec ships with pgx/v5. # # Removal Criteria: # - CrowdSec releases a version with pgx/v5 (pgproto3/v3) replacing pgproto3/v2 @@ -197,7 +200,7 @@ ignore: Charon uses SQLite, not PostgreSQL; this code path is not reachable in a standard deployment. Risk accepted; no remediation until CrowdSec ships with pgx/v5. Reviewed 2026-03-19: pgproto3/v2 EOL confirmed; CrowdSec has not migrated to pgx/v5 yet. - expiry: "2026-04-19" # 30-day review: no fix path until CrowdSec migrates to pgx/v5. + expiry: "2026-05-19" # Extended 2026-04-04: no fix path until CrowdSec migrates to pgx/v5. # Action items when this suppression expires: # 1. Check CrowdSec releases for pgx/v5 migration: @@ -245,7 +248,8 @@ ignore: # - Reviewed 2026-03-21 (initial suppression): pgproto3/v2 is EOL; no fix exists or will exist. # Waiting on CrowdSec to migrate to pgx/v5. Set 30-day review. Sibling GHSA-jqcq-xjh3-6g23 # was already suppressed; this alias surfaced as a separate Grype match via NVD/Red Hat tracking. - # - Next review: 2026-04-21. Remove suppression once CrowdSec ships with pgx/v5. + # - Extended 2026-04-04: CrowdSec has not migrated to pgx/v5 yet. + # - Next review: 2026-05-21. Remove suppression once CrowdSec ships with pgx/v5. # # Removal Criteria: # - Same as GHSA-jqcq-xjh3-6g23: CrowdSec releases a version with pgx/v5 replacing pgproto3/v2 @@ -271,7 +275,7 @@ ignore: Charon uses SQLite, not PostgreSQL; this code path is not reachable in a standard deployment. Risk accepted; no remediation until CrowdSec ships with pgx/v5. Reviewed 2026-03-21: pgproto3/v2 EOL confirmed; CrowdSec has not migrated to pgx/v5 yet. - expiry: "2026-04-21" # 30-day review: no fix path until CrowdSec migrates to pgx/v5. + expiry: "2026-05-21" # Extended 2026-04-04: no fix path until CrowdSec migrates to pgx/v5. # Action items when this suppression expires: # 1. Check CrowdSec releases for pgx/v5 migration: @@ -284,6 +288,207 @@ ignore: # 4. If not yet migrated: Extend expiry by 30 days and update the review comment above # 5. If extended 3+ times: Open an upstream issue on crowdsecurity/crowdsec requesting pgx/v5 migration + # GHSA-x744-4wpc-v9h2 / CVE-2026-34040: Docker AuthZ plugin bypass via oversized request body + # Severity: HIGH (CVSS 8.8) + # CVSS Vector: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H + # CWE: CWE-863 (Incorrect Authorization) + # Package: github.com/docker/docker v28.5.2+incompatible (go-module) + # Status: Fixed in moby/moby v29.3.1 — NO fix available for docker/docker import path + # + # Vulnerability Details: + # - Incomplete fix for Docker AuthZ plugin bypass (CVE-2024-41110). An attacker can send an + # oversized request body to the Docker daemon, causing it to forward the request to the AuthZ + # plugin without the body, allowing unauthorized approvals. + # + # Root Cause (No Fix Available for Import Path): + # - The fix exists in moby/moby v29.3.1, but not for the docker/docker import path that Charon uses. + # - Migration to moby/moby/v2 is not practical: currently beta with breaking changes. + # - Fix path: once docker/docker publishes a patched version or moby/moby/v2 stabilizes, + # update the dependency and remove this suppression. + # + # Risk Assessment: ACCEPTED (Not exploitable in Charon context) + # - Charon uses the Docker client SDK only (list containers). The vulnerability is server-side + # in the Docker daemon's AuthZ plugin handler. + # - Charon does not run a Docker daemon or use AuthZ plugins. + # - The attack vector requires local access to the Docker daemon socket with AuthZ plugins enabled. + # + # Mitigation (active while suppression is in effect): + # - Monitor docker/docker releases: https://github.com/moby/moby/releases + # - Monitor moby/moby/v2 stabilization: https://github.com/moby/moby + # - Weekly CI security rebuild flags the moment a fixed version ships. + # + # Review: + # - Reviewed 2026-03-30 (initial suppression): no fix for docker/docker import path. Set 30-day review. + # - Next review: 2026-04-30. Remove suppression once a fix is available for the docker/docker import path. + # + # Removal Criteria: + # - docker/docker publishes a patched version OR moby/moby/v2 stabilizes and migration is feasible + # - Update dependency, rebuild, run security-scan-docker-image, confirm finding is resolved + # - Remove this entry, the GHSA-pxq6-2prw-chj9 entry, and the corresponding .trivyignore entries simultaneously + # + # References: + # - GHSA-x744-4wpc-v9h2: https://github.com/advisories/GHSA-x744-4wpc-v9h2 + # - CVE-2026-34040: https://nvd.nist.gov/vuln/detail/CVE-2026-34040 + # - CVE-2024-41110 (original): https://nvd.nist.gov/vuln/detail/CVE-2024-41110 + # - moby/moby releases: https://github.com/moby/moby/releases + - vulnerability: GHSA-x744-4wpc-v9h2 + package: + name: github.com/docker/docker + version: "v28.5.2+incompatible" + type: go-module + reason: | + HIGH — Docker AuthZ plugin bypass via oversized request body in docker/docker v28.5.2+incompatible. + Incomplete fix for CVE-2024-41110. Fixed in moby/moby v29.3.1 but no fix for docker/docker import path. + Charon uses Docker client SDK only (list containers); the vulnerability is server-side in the Docker + daemon's AuthZ plugin handler. Charon does not run a Docker daemon or use AuthZ plugins. + Risk accepted; no remediation path until docker/docker publishes a fix or moby/moby/v2 stabilizes. + Reviewed 2026-03-30: no patched release available for docker/docker import path. + expiry: "2026-04-30" # 30-day review: no fix for docker/docker import path. Extend in 30-day increments with documented justification. + + # Action items when this suppression expires: + # 1. Check docker/docker and moby/moby releases: https://github.com/moby/moby/releases + # 2. Check if moby/moby/v2 has stabilized: https://github.com/moby/moby + # 3. If a fix has shipped for docker/docker import path OR moby/moby/v2 is stable: + # a. Update the dependency and rebuild Docker image + # b. Run local security-scan-docker-image and confirm finding is resolved + # c. Remove this entry, GHSA-pxq6-2prw-chj9 entry, and all corresponding .trivyignore entries + # 4. If no fix yet: Extend expiry by 30 days and update the review comment above + # 5. If extended 3+ times: Open an issue to track moby/moby/v2 migration feasibility + + # GHSA-pxq6-2prw-chj9 / CVE-2026-33997: Moby off-by-one error in plugin privilege validation + # Severity: MEDIUM (CVSS 6.8) + # Package: github.com/docker/docker v28.5.2+incompatible (go-module) + # Status: Fixed in moby/moby v29.3.1 — NO fix available for docker/docker import path + # + # Vulnerability Details: + # - Off-by-one error in Moby's plugin privilege validation allows potential privilege escalation + # via crafted plugin configurations. + # + # Root Cause (No Fix Available for Import Path): + # - Same import path issue as GHSA-x744-4wpc-v9h2. The fix exists in moby/moby v29.3.1 but not + # for the docker/docker import path that Charon uses. + # - Fix path: same as GHSA-x744-4wpc-v9h2 — wait for docker/docker patch or moby/moby/v2 stabilization. + # + # Risk Assessment: ACCEPTED (Not exploitable in Charon context) + # - Charon uses the Docker client SDK only (list containers). The vulnerability is in Docker's + # plugin privilege validation, which is server-side functionality. + # - Charon does not run a Docker daemon, install Docker plugins, or interact with plugin privileges. + # + # Mitigation (active while suppression is in effect): + # - Monitor docker/docker releases: https://github.com/moby/moby/releases + # - Weekly CI security rebuild flags the moment a fixed version ships. + # + # Review: + # - Reviewed 2026-03-30 (initial suppression): no fix for docker/docker import path. Set 30-day review. + # - Next review: 2026-04-30. Remove suppression once a fix is available for the docker/docker import path. + # + # Removal Criteria: + # - Same as GHSA-x744-4wpc-v9h2: docker/docker publishes a patched version OR moby/moby/v2 stabilizes + # - Update dependency, rebuild, run security-scan-docker-image, confirm finding is resolved + # - Remove this entry, GHSA-x744-4wpc-v9h2 entry, and all corresponding .trivyignore entries simultaneously + # + # References: + # - GHSA-pxq6-2prw-chj9: https://github.com/advisories/GHSA-pxq6-2prw-chj9 + # - CVE-2026-33997: https://nvd.nist.gov/vuln/detail/CVE-2026-33997 + # - moby/moby releases: https://github.com/moby/moby/releases + - vulnerability: GHSA-pxq6-2prw-chj9 + package: + name: github.com/docker/docker + version: "v28.5.2+incompatible" + type: go-module + reason: | + MEDIUM — Off-by-one error in Moby plugin privilege validation in docker/docker v28.5.2+incompatible. + Fixed in moby/moby v29.3.1 but no fix for docker/docker import path. + Charon uses Docker client SDK only (list containers); the vulnerability is in Docker's server-side + plugin privilege validation. Charon does not run a Docker daemon or install Docker plugins. + Risk accepted; no remediation path until docker/docker publishes a fix or moby/moby/v2 stabilizes. + Reviewed 2026-03-30: no patched release available for docker/docker import path. + expiry: "2026-04-30" # 30-day review: no fix for docker/docker import path. Extend in 30-day increments with documented justification. + + # Action items when this suppression expires: + # 1. Check docker/docker and moby/moby releases: https://github.com/moby/moby/releases + # 2. Check if moby/moby/v2 has stabilized: https://github.com/moby/moby + # 3. If a fix has shipped for docker/docker import path OR moby/moby/v2 is stable: + # a. Update the dependency and rebuild Docker image + # b. Run local security-scan-docker-image and confirm finding is resolved + # c. Remove this entry, GHSA-x744-4wpc-v9h2 entry, and all corresponding .trivyignore entries + # 4. If no fix yet: Extend expiry by 30 days and update the review comment above + # 5. If extended 3+ times: Open an issue to track moby/moby/v2 migration feasibility + + # GHSA-78h2-9frx-2jm8: go-jose JWE decryption panic (DoS) + # Severity: HIGH + # Packages: github.com/go-jose/go-jose/v3 v3.0.4 and github.com/go-jose/go-jose/v4 v4.1.3 + # (embedded in /usr/bin/caddy) + # Status: Fix available in go-jose/v3 v3.0.5 and go-jose/v4 v4.1.4 — requires upstream Caddy rebuild + # + # Vulnerability Details: + # - JWE decryption can trigger a panic due to improper input validation, causing + # a denial-of-service condition (runtime crash). + # + # Root Cause (Third-Party Binary): + # - Charon does not use go-jose directly. The library is compiled into the Caddy binary + # shipped in the Docker image. + # - Fixes are available upstream (v3.0.5 and v4.1.4) but require a Caddy rebuild to pick up. + # - Fix path: once the upstream Caddy release includes the patched go-jose versions, + # rebuild the Docker image and remove these suppressions. + # + # Risk Assessment: ACCEPTED (No direct use + fix requires upstream rebuild) + # - Charon does not import or call go-jose functions; the library is only present as a + # transitive dependency inside the Caddy binary. + # - The attack vector requires crafted JWE input reaching Caddy's internal JWT handling, + # which is limited to authenticated admin-API paths not exposed in Charon deployments. + # + # Mitigation (active while suppression is in effect): + # - Monitor Caddy releases: https://github.com/caddyserver/caddy/releases + # - Weekly CI security rebuild flags the moment a fixed image ships. + # + # Review: + # - Reviewed 2026-04-05 (initial suppression): fix available upstream but not yet in Caddy release. + # Set 30-day review. + # - Next review: 2026-05-05. Remove suppression once Caddy ships with patched go-jose. + # + # Removal Criteria: + # - Caddy releases a version built with go-jose/v3 >= v3.0.5 and go-jose/v4 >= v4.1.4 + # - Rebuild Docker image, run security-scan-docker-image, confirm finding is resolved + # - Remove both entries (v3 and v4) and any corresponding .trivyignore entries simultaneously + # + # References: + # - GHSA-78h2-9frx-2jm8: https://github.com/advisories/GHSA-78h2-9frx-2jm8 + # - go-jose releases: https://github.com/go-jose/go-jose/releases + # - Caddy releases: https://github.com/caddyserver/caddy/releases + - vulnerability: GHSA-78h2-9frx-2jm8 + package: + name: github.com/go-jose/go-jose/v3 + version: "v3.0.4" + type: go-module + reason: | + HIGH — JWE decryption panic in go-jose v3.0.4 embedded in /usr/bin/caddy. + Fix available in v3.0.5 but requires upstream Caddy rebuild. Charon does not use go-jose + directly. Deferring to next Caddy release. + expiry: "2026-05-05" # 30-day review: remove once Caddy ships with go-jose/v3 >= v3.0.5. + + # Action items when this suppression expires: + # 1. Check Caddy releases: https://github.com/caddyserver/caddy/releases + # 2. Verify with: `go version -m /usr/bin/caddy | grep go-jose` + # Expected: go-jose/v3 >= v3.0.5 + # 3. If Caddy has updated: + # a. Rebuild Docker image and run local security-scan-docker-image + # b. Remove this entry, the v4 entry below, and any corresponding .trivyignore entries + # 4. If not yet updated: Extend expiry by 30 days and update the review comment above + # 5. If extended 3+ times: Open an upstream issue on caddyserver/caddy requesting go-jose update + + # GHSA-78h2-9frx-2jm8 (go-jose/v4) — see full justification in the go-jose/v3 entry above + - vulnerability: GHSA-78h2-9frx-2jm8 + package: + name: github.com/go-jose/go-jose/v4 + version: "v4.1.3" + type: go-module + reason: | + HIGH — JWE decryption panic in go-jose v4.1.3 embedded in /usr/bin/caddy. + Fix available in v4.1.4 but requires upstream Caddy rebuild. Charon does not use go-jose + directly. Deferring to next Caddy release. + expiry: "2026-05-05" # 30-day review: see go-jose/v3 entry above for action items. + # Match exclusions (patterns to ignore during scanning) # Use sparingly - prefer specific CVE suppressions above match: diff --git a/.trivyignore b/.trivyignore index 199b38ecb..e33610032 100644 --- a/.trivyignore +++ b/.trivyignore @@ -19,8 +19,8 @@ CVE-2026-22184 # Severity: MEDIUM (CVSS 5.5 NVD / 2.9 MITRE) — Package: zlib 1.3.1-r2 in Alpine base image # Fix requires zlib >= 1.3.2. No upstream fix available: Alpine 3.23 still ships zlib 1.3.1-r2. # Attack requires local access (AV:L); the vulnerable code path is not reachable via Charon's -# network-facing surface. Non-blocking by CI policy (MEDIUM). Review by: 2026-04-21 -# exp: 2026-04-21 +# network-facing surface. Non-blocking by CI policy (MEDIUM). Review by: 2026-05-21 +# exp: 2026-05-21 CVE-2026-27171 # CVE-2026-2673: OpenSSL TLS 1.3 server key exchange group downgrade (libcrypto3/libssl3) @@ -28,45 +28,47 @@ CVE-2026-27171 # No upstream fix available: Alpine 3.23 still ships libcrypto3/libssl3 3.5.5-r0 as of 2026-03-18. # When DEFAULT is in TLS 1.3 group config, server may select a weaker key exchange group. # Charon terminates TLS at the Caddy layer — the Go backend does not act as a raw TLS 1.3 server. -# Review by: 2026-04-18 +# Review by: 2026-05-18 # See also: .grype.yaml for full justification -# exp: 2026-04-18 +# exp: 2026-05-18 CVE-2026-2673 # CVE-2026-33186 / GHSA-p77j-4mvh-x3m3: gRPC-Go authorization bypass via missing leading slash # Severity: CRITICAL (CVSS 9.1) — Package: google.golang.org/grpc, embedded in CrowdSec (v1.74.2) and Caddy (v1.79.1) # Fix exists at v1.79.3 — Charon's own dep is patched. Waiting on CrowdSec and Caddy upstream releases. # CrowdSec's and Caddy's grpc servers are not exposed externally in a standard Charon deployment. -# Review by: 2026-04-02 +# Suppressed for CrowdSec/Caddy embedded binaries only — Charon's direct deps are fixed (v1.79.3). +# Review by: 2026-05-04 # See also: .grype.yaml for full justification -# exp: 2026-04-02 +# exp: 2026-05-04 CVE-2026-33186 # GHSA-479m-364c-43vc: goxmldsig XML signature validation bypass (loop variable capture) # Severity: HIGH (CVSS 7.5) — Package: github.com/russellhaering/goxmldsig v1.5.0, embedded in /usr/bin/caddy # Fix exists at v1.6.0 — waiting on Caddy upstream (or caddy-security plugin) to release with patched goxmldsig. # Charon does not configure SAML-based SSO by default; the vulnerable path is not reachable in a standard deployment. -# Review by: 2026-04-02 +# Awaiting Caddy upstream update to include goxmldsig v1.6.0. +# Review by: 2026-05-04 # See also: .grype.yaml for full justification -# exp: 2026-04-02 +# exp: 2026-05-04 GHSA-479m-364c-43vc # GHSA-6g7g-w4f8-9c9x: buger/jsonparser Delete panic on malformed JSON (DoS) # Severity: HIGH (CVSS 7.5) — Package: github.com/buger/jsonparser v1.1.1, embedded in CrowdSec binaries # No upstream fix available as of 2026-03-19 (issue #275 open, golang/vulndb #4514 open). # Charon does not use this package; the vector requires reaching CrowdSec's internal processing pipeline. -# Review by: 2026-04-19 +# Review by: 2026-05-19 # See also: .grype.yaml for full justification -# exp: 2026-04-19 +# exp: 2026-05-19 GHSA-6g7g-w4f8-9c9x # GHSA-jqcq-xjh3-6g23: pgproto3/v2 DataRow.Decode panic on negative field length (DoS) # Severity: HIGH (CVSS 7.5) — Package: github.com/jackc/pgproto3/v2 v2.3.3, embedded in CrowdSec binaries # pgproto3/v2 is archived/EOL — no fix will be released. Fix path requires CrowdSec to migrate to pgx/v5. # Charon uses SQLite; the PostgreSQL code path is not reachable in a standard deployment. -# Review by: 2026-04-19 +# Review by: 2026-05-19 # See also: .grype.yaml for full justification -# exp: 2026-04-19 +# exp: 2026-05-19 GHSA-jqcq-xjh3-6g23 # GHSA-x6gf-mpr2-68h6 / CVE-2026-4427: pgproto3/v2 DataRow.Decode panic on negative field length (DoS) @@ -74,7 +76,41 @@ GHSA-jqcq-xjh3-6g23 # NVD/Red Hat alias (CVE-2026-4427) for the same underlying bug as GHSA-jqcq-xjh3-6g23. # pgproto3/v2 is archived/EOL — no fix will be released. Fix path requires CrowdSec to migrate to pgx/v5. # Charon uses SQLite; the PostgreSQL code path is not reachable in a standard deployment. -# Review by: 2026-04-21 +# Review by: 2026-05-21 # See also: .grype.yaml for full justification -# exp: 2026-04-21 +# exp: 2026-05-21 GHSA-x6gf-mpr2-68h6 + +# CVE-2026-34040 / GHSA-x744-4wpc-v9h2: Docker AuthZ plugin bypass via oversized request body +# Severity: HIGH (CVSS 8.8) — Package: github.com/docker/docker v28.5.2+incompatible +# Incomplete fix for CVE-2024-41110. Fixed in moby/moby v29.3.1 but no fix for docker/docker import path. +# Charon uses Docker client SDK only (list containers); the vulnerability is server-side in the Docker daemon. +# Review by: 2026-04-30 +# See also: .grype.yaml for full justification +# exp: 2026-04-30 +CVE-2026-34040 + +# GHSA-x744-4wpc-v9h2: Docker AuthZ plugin bypass via oversized request body (GHSA alias) +# Severity: HIGH (CVSS 8.8) — Package: github.com/docker/docker v28.5.2+incompatible +# GHSA alias for CVE-2026-34040. See CVE-2026-34040 entry above for full details. +# Review by: 2026-04-30 +# See also: .grype.yaml for full justification +# exp: 2026-04-30 +GHSA-x744-4wpc-v9h2 + +# CVE-2026-33997 / GHSA-pxq6-2prw-chj9: Moby off-by-one error in plugin privilege validation +# Severity: MEDIUM (CVSS 6.8) — Package: github.com/docker/docker v28.5.2+incompatible +# Fixed in moby/moby v29.3.1 but no fix for docker/docker import path. +# Charon uses Docker client SDK only (list containers); plugin privilege validation is server-side. +# Review by: 2026-04-30 +# See also: .grype.yaml for full justification +# exp: 2026-04-30 +CVE-2026-33997 + +# GHSA-pxq6-2prw-chj9: Moby off-by-one error in plugin privilege validation (GHSA alias) +# Severity: MEDIUM (CVSS 6.8) — Package: github.com/docker/docker v28.5.2+incompatible +# GHSA alias for CVE-2026-33997. See CVE-2026-33997 entry above for full details. +# Review by: 2026-04-30 +# See also: .grype.yaml for full justification +# exp: 2026-04-30 +GHSA-pxq6-2prw-chj9 diff --git a/CHANGELOG.md b/CHANGELOG.md index edcc6bd2d..7474d9e77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **CrowdSec Dashboard**: Visual analytics for CrowdSec security data within the Security section + - Summary cards showing total bans, active bans, unique IPs, and top scenario + - Interactive charts: ban timeline (area), top attacking IPs (bar), scenario breakdown (pie) + - Configurable time range selector (1h, 6h, 24h, 7d, 30d) + - Active decisions table with IP, scenario, duration, type, and time remaining + - Alerts feed with pagination sourced from CrowdSec LAPI + - CSV and JSON export for decisions data + - Server-side caching (30–60s TTL) for fast dashboard loads + - Full i18n support across all 5 locales (en, de, fr, es, zh) + - Keyboard navigable, screen-reader compatible (WCAG 2.2 AA) + - **Notifications:** Added Ntfy notification provider with support for self-hosted and cloud instances, optional Bearer token authentication, and JSON template customization - **Certificate Deletion**: Clean up expired and unused certificates directly from the Certificates page diff --git a/Dockerfile b/Dockerfile index 14b013c28..aba2b6989 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ ARG ALPINE_IMAGE=alpine:3.23.3@sha256:25109184c71bdad752c8312a8623239686a9a2071e # ---- Shared CrowdSec Version ---- # renovate: datasource=github-releases depName=crowdsecurity/crowdsec -ARG CROWDSEC_VERSION=1.7.6 +ARG CROWDSEC_VERSION=1.7.7 # CrowdSec fallback tarball checksum (v${CROWDSEC_VERSION}) ARG CROWDSEC_RELEASE_SHA256=704e37121e7ac215991441cef0d8732e33fa3b1a2b2b88b53a0bfe5e38f863bd @@ -43,9 +43,9 @@ ARG CADDY_CANDIDATE_VERSION=2.11.2 ARG CADDY_USE_CANDIDATE=0 ARG CADDY_PATCH_SCENARIO=B # renovate: datasource=go depName=github.com/greenpau/caddy-security -ARG CADDY_SECURITY_VERSION=1.1.51 +ARG CADDY_SECURITY_VERSION=1.1.61 # renovate: datasource=go depName=github.com/corazawaf/coraza-caddy -ARG CORAZA_CADDY_VERSION=2.2.0 +ARG CORAZA_CADDY_VERSION=2.4.0 ## When an official caddy image tag isn't available on the host, use a ## plain Alpine base image and overwrite its caddy binary with our ## xcaddy-built binary in the later COPY step. This avoids relying on @@ -92,7 +92,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ # ---- Frontend Builder ---- # Build the frontend using the BUILDPLATFORM to avoid arm64 musl Rollup native issues # renovate: datasource=docker depName=node -FROM --platform=$BUILDPLATFORM node:24.14.0-alpine@sha256:7fddd9ddeae8196abf4a3ef2de34e11f7b1a722119f91f28ddf1e99dcafdf114 AS frontend-builder +FROM --platform=$BUILDPLATFORM node:24.14.1-alpine@sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b AS frontend-builder WORKDIR /app/frontend # Copy frontend package files @@ -364,7 +364,7 @@ RUN go get github.com/expr-lang/expr@v${EXPR_LANG_VERSION} && \ # Fix available at v1.79.3. Pin here so the CrowdSec binary is patched immediately; # remove once CrowdSec ships a release built with grpc >= v1.79.3. # renovate: datasource=go depName=google.golang.org/grpc - go get google.golang.org/grpc@v1.79.3 && \ + go get google.golang.org/grpc@v1.80.0 && \ go mod tidy # Fix compatibility issues with expr-lang v1.17.7 diff --git a/README.md b/README.md index 776b95a66..6955efc8c 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ If you can use a website, you can run Charon. Charon includes security features that normally require multiple tools: - Web Application Firewall (WAF) -- CrowdSec intrusion detection +- CrowdSec intrusion detection with analytics dashboard - Access Control Lists (ACLs) - Rate limiting - Emergency recovery tools @@ -148,7 +148,7 @@ Secure all your subdomains with a single *.example.com certificate. Supports 15+ ### 🛡️ **Enterprise-Grade Security Built In** -Web Application Firewall, rate limiting, geographic blocking, access control lists, and intrusion detection via CrowdSec. Protection that "just works." +Web Application Firewall, rate limiting, geographic blocking, access control lists, and intrusion detection via CrowdSec—with a built-in analytics dashboard showing attack trends, top offenders, and ban history. Protection that "just works." ### 🔐 **Supply Chain Security** diff --git a/SECURITY.md b/SECURITY.md index ec4df8b2c..c6f81b83d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -27,7 +27,7 @@ public disclosure. ## Known Vulnerabilities -Last reviewed: 2026-03-24 +Last reviewed: 2026-04-04 ### [HIGH] CVE-2026-2673 · OpenSSL TLS 1.3 Key Exchange Group Downgrade @@ -73,6 +73,48 @@ available, update the pinned `ALPINE_IMAGE` digest in the Dockerfile, or add an --- +### [HIGH] CVE-2026-34040 · Docker AuthZ Plugin Bypass via Oversized Request Body + +| Field | Value | +|--------------|-------| +| **ID** | CVE-2026-34040 (GHSA-x744-4wpc-v9h2) | +| **Severity** | High · 8.8 | +| **Status** | Awaiting Upstream | + +**What** +Docker Engine AuthZ plugins can be bypassed when an API request body exceeds a +certain size threshold. Charon uses the Docker client SDK only; this is a +server-side vulnerability in the Docker daemon's authorization plugin handler. + +**Who** + +- Discovered by: Automated scan (govulncheck, Grype) +- Reported: 2026-04-04 +- Affects: Docker Engine daemon operators; Charon application is not directly vulnerable + +**Where** + +- Component: `github.com/docker/docker` v28.5.2+incompatible (Docker client SDK) +- Versions affected: Docker Engine < 29.3.1 + +**When** + +- Discovered: 2026-04-04 +- Disclosed (if public): Public +- Target fix: When moby/moby/v2 stabilizes or docker/docker import path is updated + +**How** +The vulnerability requires an attacker to send oversized API request bodies to the +Docker daemon. Charon uses the Docker client SDK for container management operations +only and does not expose the Docker socket externally. The attack vector is limited +to the Docker daemon host, not the Charon application. + +**Planned Remediation** +Monitor moby/moby/v2 module stabilization. The `docker/docker` import path has no +fix available. When a compatible module path exists, migrate the Docker SDK import. + +--- + ### [MEDIUM] CVE-2025-60876 · BusyBox wget HTTP Request Smuggling | Field | Value | @@ -113,13 +155,57 @@ Charon users is negligible since the vulnerable code path is not exercised. --- -### [LOW] CVE-2026-26958 · edwards25519 MultiScalarMult Invalid Results +### [MEDIUM] CVE-2026-33997 · Docker Off-by-One Plugin Privilege Validation + +| Field | Value | +|--------------|-------| +| **ID** | CVE-2026-33997 (GHSA-pxq6-2prw-chj9) | +| **Severity** | Medium · 6.8 | +| **Status** | Awaiting Upstream | + +**What** +An off-by-one error in Docker Engine's plugin privilege validation could allow +a malicious plugin to escalate privileges. Charon uses the Docker client SDK +for container management and does not install or manage Docker plugins. + +**Who** + +- Discovered by: Automated scan (govulncheck, Grype) +- Reported: 2026-04-04 +- Affects: Docker Engine plugin operators; Charon application is not directly vulnerable + +**Where** + +- Component: `github.com/docker/docker` v28.5.2+incompatible (Docker client SDK) +- Versions affected: Docker Engine < 29.3.1 + +**When** + +- Discovered: 2026-04-04 +- Disclosed (if public): Public +- Target fix: When moby/moby/v2 stabilizes or docker/docker import path is updated + +**How** +The vulnerability is in Docker Engine's plugin privilege validation at the +daemon level. Charon does not use Docker plugins — it only manages containers +via the Docker client SDK. The attack requires a malicious Docker plugin to be +installed on the host, which is outside Charon's operational scope. + +**Planned Remediation** +Same as CVE-2026-34040: monitor moby/moby/v2 module stabilization. No fix +available for the current `docker/docker` import path. + +--- + +## Patched Vulnerabilities + +### ✅ [LOW] CVE-2026-26958 · edwards25519 MultiScalarMult Invalid Results | Field | Value | |--------------|-------| | **ID** | CVE-2026-26958 (GHSA-fw7p-63qq-7hpr) | | **Severity** | Low · 1.7 | -| **Status** | Awaiting Upstream | +| **Patched** | 2026-04-04 | **What** `filippo.io/edwards25519` v1.1.0 `MultiScalarMult` produces invalid results or undefined @@ -130,8 +216,6 @@ CrowdSec to rebuild. - Discovered by: Automated scan (Grype) - Reported: 2026-03-24 -- Affects: CrowdSec Agent component within the container; not directly exposed through Charon's - primary application interface **Where** @@ -141,21 +225,19 @@ CrowdSec to rebuild. **When** - Discovered: 2026-03-24 -- Disclosed (if public): Public -- Target fix: When CrowdSec releases a build with updated dependency +- Patched: 2026-04-04 +- Time to patch: 11 days **How** This is a rarely used advanced API within the edwards25519 library. CrowdSec does not directly expose MultiScalarMult to external input. EPSS score is 0.00018 (0.04 percentile). -**Planned Remediation** -Awaiting CrowdSec upstream release with updated dependency. No action available for Charon -maintainers. +**Resolution** +Dependency no longer present in Charon's dependency tree. CrowdSec binaries no longer bundle +affected version. --- -## Patched Vulnerabilities - ### ✅ [CRITICAL] CVE-2025-68121 · Go Stdlib Critical in CrowdSec Bundled Binaries | Field | Value | diff --git a/backend/go.mod b/backend/go.mod index 44a2e22b3..d452e4604 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,13 +4,13 @@ go 1.26.1 require ( github.com/docker/docker v28.5.2+incompatible - github.com/gin-contrib/gzip v1.2.5 + github.com/gin-contrib/gzip v1.2.6 github.com/gin-gonic/gin v1.12.0 github.com/glebarez/sqlite v1.11.0 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 - github.com/mattn/go-sqlite3 v1.14.37 + github.com/mattn/go-sqlite3 v1.14.40 github.com/oschwald/geoip2-golang/v2 v2.1.0 github.com/prometheus/client_golang v1.23.2 github.com/robfig/cron/v3 v3.0.1 @@ -30,7 +30,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/gopkg v0.1.4 // indirect github.com/bytedance/sonic v1.15.0 // indirect - github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/bytedance/sonic/loader v0.5.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/containerd/errdefs v1.0.0 // indirect @@ -43,13 +43,13 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect - github.com/gin-contrib/sse v1.1.0 // indirect + github.com/gin-contrib/sse v1.1.1 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/go-playground/validator/v10 v10.30.2 // indirect github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect @@ -85,10 +85,10 @@ require ( go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect - go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 // indirect - go.opentelemetry.io/otel/metric v1.42.0 // indirect - go.opentelemetry.io/otel/trace v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect golang.org/x/arch v0.25.0 // indirect golang.org/x/sys v0.42.0 // indirect @@ -99,5 +99,5 @@ require ( modernc.org/libc v1.70.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.47.0 // indirect + modernc.org/sqlite v1.48.1 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 5c30b3062..11e3d3e32 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -8,8 +8,8 @@ github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= -github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= -github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI= +github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -39,10 +39,10 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI= -github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw= -github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= -github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-contrib/gzip v1.2.6 h1:OtN8DplD5DNZCSLAnQ5HxRkD2qZ5VU+JhOrcfJrcRvg= +github.com/gin-contrib/gzip v1.2.6/go.mod h1:BQy8/+JApnRjAVUplSGZiVtD2k8GmIE2e9rYu/hLzzU= +github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko= +github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= @@ -60,8 +60,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= -github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= +github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= @@ -101,8 +101,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg= -github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.40 h1:f7+saIsbq4EF86mUqe0uiecQOJYMOdfi5uATADmUG94= +github.com/mattn/go-sqlite3 v1.14.40/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= @@ -179,20 +179,20 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= -go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= -go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc= -go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= -go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= -go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= -go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -264,8 +264,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk= -modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= +modernc.org/sqlite v1.48.1 h1:S85iToyU6cgeojybE2XJlSbcsvcWkQ6qqNXJHtW5hWA= +modernc.org/sqlite v1.48.1/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/backend/internal/api/handlers/access_list_handler_coverage_test.go b/backend/internal/api/handlers/access_list_handler_coverage_test.go index 19ff63f1b..fac2cac58 100644 --- a/backend/internal/api/handlers/access_list_handler_coverage_test.go +++ b/backend/internal/api/handlers/access_list_handler_coverage_test.go @@ -121,7 +121,6 @@ func TestAccessListHandler_List_DBError(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) // Don't migrate the table to cause error - gin.SetMode(gin.TestMode) router := gin.New() handler := NewAccessListHandler(db) @@ -138,7 +137,6 @@ func TestAccessListHandler_Get_DBError(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) // Don't migrate the table to cause error - gin.SetMode(gin.TestMode) router := gin.New() handler := NewAccessListHandler(db) @@ -157,7 +155,6 @@ func TestAccessListHandler_Delete_InternalError(t *testing.T) { // Migrate AccessList but not ProxyHost to cause internal error on delete _ = db.AutoMigrate(&models.AccessList{}) - gin.SetMode(gin.TestMode) router := gin.New() handler := NewAccessListHandler(db) @@ -285,7 +282,6 @@ func TestAccessListHandler_TestIP_InternalError(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) // Don't migrate - this causes a "no such table" error which is an internal error - gin.SetMode(gin.TestMode) router := gin.New() handler := NewAccessListHandler(db) diff --git a/backend/internal/api/handlers/access_list_handler_test.go b/backend/internal/api/handlers/access_list_handler_test.go index 1bf899788..b2f22fb99 100644 --- a/backend/internal/api/handlers/access_list_handler_test.go +++ b/backend/internal/api/handlers/access_list_handler_test.go @@ -21,7 +21,6 @@ func setupAccessListTestRouter(t *testing.T) (*gin.Engine, *gorm.DB) { err = db.AutoMigrate(&models.AccessList{}, &models.ProxyHost{}) assert.NoError(t, err) - gin.SetMode(gin.TestMode) router := gin.New() handler := NewAccessListHandler(db) diff --git a/backend/internal/api/handlers/additional_coverage_test.go b/backend/internal/api/handlers/additional_coverage_test.go index 63b95a1f7..0b4eb9696 100644 --- a/backend/internal/api/handlers/additional_coverage_test.go +++ b/backend/internal/api/handlers/additional_coverage_test.go @@ -27,7 +27,6 @@ func setupImportCoverageDB(t *testing.T) *gorm.DB { } func TestImportHandler_Commit_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportCoverageDB(t) h := NewImportHandler(db, "", t.TempDir(), "") @@ -44,7 +43,6 @@ func TestImportHandler_Commit_InvalidJSON(t *testing.T) { } func TestImportHandler_Commit_InvalidSessionUUID(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportCoverageDB(t) h := NewImportHandler(db, "", t.TempDir(), "") @@ -67,7 +65,6 @@ func TestImportHandler_Commit_InvalidSessionUUID(t *testing.T) { } func TestImportHandler_Commit_SessionNotFound(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportCoverageDB(t) h := NewImportHandler(db, "", t.TempDir(), "") @@ -98,7 +95,6 @@ func setupRemoteServerCoverageDB2(t *testing.T) *gorm.DB { } func TestRemoteServerHandler_TestConnection_Unreachable(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupRemoteServerCoverageDB2(t) svc := services.NewRemoteServerService(db) h := NewRemoteServerHandler(svc, nil) @@ -137,7 +133,6 @@ func setupSecurityCoverageDB3(t *testing.T) *gorm.DB { } func TestSecurityHandler_GetConfig_InternalError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSecurityCoverageDB3(t) h := NewSecurityHandler(config.SecurityConfig{}, db, nil) @@ -157,7 +152,6 @@ func TestSecurityHandler_GetConfig_InternalError(t *testing.T) { } func TestSecurityHandler_UpdateConfig_ApplyCaddyError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSecurityCoverageDB3(t) // Create handler with nil caddy manager (ApplyConfig will be called but is nil) @@ -181,7 +175,6 @@ func TestSecurityHandler_UpdateConfig_ApplyCaddyError(t *testing.T) { } func TestSecurityHandler_GenerateBreakGlass_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSecurityCoverageDB3(t) h := NewSecurityHandler(config.SecurityConfig{}, db, nil) @@ -201,7 +194,6 @@ func TestSecurityHandler_GenerateBreakGlass_Error(t *testing.T) { } func TestSecurityHandler_ListDecisions_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSecurityCoverageDB3(t) h := NewSecurityHandler(config.SecurityConfig{}, db, nil) @@ -220,7 +212,6 @@ func TestSecurityHandler_ListDecisions_Error(t *testing.T) { } func TestSecurityHandler_ListRuleSets_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSecurityCoverageDB3(t) h := NewSecurityHandler(config.SecurityConfig{}, db, nil) @@ -239,7 +230,6 @@ func TestSecurityHandler_ListRuleSets_Error(t *testing.T) { } func TestSecurityHandler_UpsertRuleSet_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSecurityCoverageDB3(t) h := NewSecurityHandler(config.SecurityConfig{}, db, nil) @@ -265,7 +255,6 @@ func TestSecurityHandler_UpsertRuleSet_Error(t *testing.T) { } func TestSecurityHandler_CreateDecision_LogError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSecurityCoverageDB3(t) h := NewSecurityHandler(config.SecurityConfig{}, db, nil) @@ -291,7 +280,6 @@ func TestSecurityHandler_CreateDecision_LogError(t *testing.T) { } func TestSecurityHandler_DeleteRuleSet_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSecurityCoverageDB3(t) h := NewSecurityHandler(config.SecurityConfig{}, db, nil) @@ -313,7 +301,6 @@ func TestSecurityHandler_DeleteRuleSet_Error(t *testing.T) { // CrowdSec ImportConfig additional coverage tests func TestCrowdsec_ImportConfig_EmptyUpload(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -344,7 +331,6 @@ func TestCrowdsec_ImportConfig_EmptyUpload(t *testing.T) { // Backup Handler additional coverage tests func TestBackupHandler_List_DBError(t *testing.T) { - gin.SetMode(gin.TestMode) // Use a non-writable temp dir to simulate errors tmpDir := t.TempDir() @@ -370,7 +356,6 @@ func TestBackupHandler_List_DBError(t *testing.T) { // ImportHandler UploadMulti coverage tests func TestImportHandler_UploadMulti_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportCoverageDB(t) h := NewImportHandler(db, "", t.TempDir(), "") @@ -387,7 +372,6 @@ func TestImportHandler_UploadMulti_InvalidJSON(t *testing.T) { } func TestImportHandler_UploadMulti_MissingCaddyfile(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportCoverageDB(t) h := NewImportHandler(db, "", t.TempDir(), "") @@ -411,7 +395,6 @@ func TestImportHandler_UploadMulti_MissingCaddyfile(t *testing.T) { } func TestImportHandler_UploadMulti_EmptyContent(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportCoverageDB(t) h := NewImportHandler(db, "", t.TempDir(), "") @@ -435,7 +418,6 @@ func TestImportHandler_UploadMulti_EmptyContent(t *testing.T) { } func TestImportHandler_UploadMulti_PathTraversal(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportCoverageDB(t) h := NewImportHandler(db, "", t.TempDir(), "") @@ -481,7 +463,6 @@ func setupLogsDownloadTest(t *testing.T) (h *LogsHandler, logsDir string) { } func TestLogsHandler_Download_PathTraversal(t *testing.T) { - gin.SetMode(gin.TestMode) h, _ := setupLogsDownloadTest(t) w := httptest.NewRecorder() @@ -496,7 +477,6 @@ func TestLogsHandler_Download_PathTraversal(t *testing.T) { } func TestLogsHandler_Download_NotFound(t *testing.T) { - gin.SetMode(gin.TestMode) h, _ := setupLogsDownloadTest(t) w := httptest.NewRecorder() @@ -511,7 +491,6 @@ func TestLogsHandler_Download_NotFound(t *testing.T) { } func TestLogsHandler_Download_Success(t *testing.T) { - gin.SetMode(gin.TestMode) h, logsDir := setupLogsDownloadTest(t) // Create a log file to download @@ -531,7 +510,6 @@ func TestLogsHandler_Download_Success(t *testing.T) { // Import Handler Upload error tests func TestImportHandler_Upload_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportCoverageDB(t) h := NewImportHandler(db, "", t.TempDir(), "") @@ -548,7 +526,6 @@ func TestImportHandler_Upload_InvalidJSON(t *testing.T) { } func TestImportHandler_Upload_EmptyContent(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportCoverageDB(t) h := NewImportHandler(db, "", t.TempDir(), "") @@ -571,7 +548,6 @@ func TestImportHandler_Upload_EmptyContent(t *testing.T) { // Additional Backup Handler tests func TestBackupHandler_List_ServiceError(t *testing.T) { - gin.SetMode(gin.TestMode) // Create a temp dir with invalid permission for backup dir tmpDir := t.TempDir() @@ -608,7 +584,6 @@ func TestBackupHandler_List_ServiceError(t *testing.T) { } func TestBackupHandler_Delete_PathTraversal(t *testing.T) { - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") @@ -639,7 +614,6 @@ func TestBackupHandler_Delete_PathTraversal(t *testing.T) { } func TestBackupHandler_Delete_InternalError2(t *testing.T) { - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") @@ -689,7 +663,6 @@ func TestBackupHandler_Delete_InternalError2(t *testing.T) { // Remote Server TestConnection error paths func TestRemoteServerHandler_TestConnection_NotFound2(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupRemoteServerCoverageDB2(t) svc := services.NewRemoteServerService(db) h := NewRemoteServerHandler(svc, nil) @@ -704,7 +677,6 @@ func TestRemoteServerHandler_TestConnection_NotFound2(t *testing.T) { } func TestRemoteServerHandler_TestConnectionCustom_Unreachable2(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupRemoteServerCoverageDB2(t) svc := services.NewRemoteServerService(db) h := NewRemoteServerHandler(svc, nil) @@ -735,7 +707,6 @@ func setupAuthCoverageDB(t *testing.T) *gorm.DB { } func TestAuthHandler_Register_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuthCoverageDB(t) cfg := config.Config{JWTSecret: "test-secret"} @@ -755,7 +726,6 @@ func TestAuthHandler_Register_InvalidJSON(t *testing.T) { // Health handler coverage func TestHealthHandler_Basic(t *testing.T) { - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -771,7 +741,6 @@ func TestHealthHandler_Basic(t *testing.T) { // Backup Create error coverage func TestBackupHandler_Create_Error(t *testing.T) { - gin.SetMode(gin.TestMode) // Use a path where database file doesn't exist tmpDir := t.TempDir() @@ -811,7 +780,6 @@ func setupSettingsCoverageDB(t *testing.T) *gorm.DB { } func TestSettingsHandler_GetSettings_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsCoverageDB(t) h := NewSettingsHandler(db) @@ -830,7 +798,6 @@ func TestSettingsHandler_GetSettings_Error(t *testing.T) { } func TestSettingsHandler_UpdateSetting_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsCoverageDB(t) h := NewSettingsHandler(db) @@ -849,7 +816,6 @@ func TestSettingsHandler_UpdateSetting_InvalidJSON(t *testing.T) { // Additional remote server TestConnection tests func TestRemoteServerHandler_TestConnection_Reachable(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupRemoteServerCoverageDB2(t) svc := services.NewRemoteServerService(db) h := NewRemoteServerHandler(svc, nil) @@ -873,7 +839,6 @@ func TestRemoteServerHandler_TestConnection_Reachable(t *testing.T) { } func TestRemoteServerHandler_TestConnection_EmptyHost(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupRemoteServerCoverageDB2(t) svc := services.NewRemoteServerService(db) h := NewRemoteServerHandler(svc, nil) @@ -900,7 +865,6 @@ func TestRemoteServerHandler_TestConnection_EmptyHost(t *testing.T) { // Additional UploadMulti test with valid Caddyfile content func TestImportHandler_UploadMulti_ValidCaddyfile(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportCoverageDB(t) h := NewImportHandler(db, "", t.TempDir(), "") @@ -925,7 +889,6 @@ func TestImportHandler_UploadMulti_ValidCaddyfile(t *testing.T) { } func TestImportHandler_UploadMulti_SubdirFile(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportCoverageDB(t) h := NewImportHandler(db, "", t.TempDir(), "") diff --git a/backend/internal/api/handlers/audit_log_handler_test.go b/backend/internal/api/handlers/audit_log_handler_test.go index 1c3378513..4a730e331 100644 --- a/backend/internal/api/handlers/audit_log_handler_test.go +++ b/backend/internal/api/handlers/audit_log_handler_test.go @@ -30,7 +30,6 @@ func setupAuditLogTestDB(t *testing.T) *gorm.DB { } func TestAuditLogHandler_List(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditLogTestDB(t) securityService := services.NewSecurityService(db) defer securityService.Close() @@ -130,7 +129,6 @@ func TestAuditLogHandler_List(t *testing.T) { } func TestAuditLogHandler_Get(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditLogTestDB(t) securityService := services.NewSecurityService(db) defer securityService.Close() @@ -198,7 +196,6 @@ func TestAuditLogHandler_Get(t *testing.T) { } func TestAuditLogHandler_ListByProvider(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditLogTestDB(t) securityService := services.NewSecurityService(db) defer securityService.Close() @@ -286,7 +283,6 @@ func TestAuditLogHandler_ListByProvider(t *testing.T) { } func TestAuditLogHandler_ListWithDateFilters(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditLogTestDB(t) securityService := services.NewSecurityService(db) defer securityService.Close() @@ -371,7 +367,6 @@ func TestAuditLogHandler_ListWithDateFilters(t *testing.T) { // TestAuditLogHandler_ServiceErrors tests error handling when service layer fails func TestAuditLogHandler_ServiceErrors(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditLogTestDB(t) securityService := services.NewSecurityService(db) defer securityService.Close() @@ -422,7 +417,6 @@ func TestAuditLogHandler_ServiceErrors(t *testing.T) { // TestAuditLogHandler_List_PaginationBoundaryEdgeCases tests pagination boundary edge cases func TestAuditLogHandler_List_PaginationBoundaryEdgeCases(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditLogTestDB(t) securityService := services.NewSecurityService(db) defer securityService.Close() @@ -513,7 +507,6 @@ func TestAuditLogHandler_List_PaginationBoundaryEdgeCases(t *testing.T) { // TestAuditLogHandler_ListByProvider_PaginationBoundaryEdgeCases tests pagination boundary edge cases for provider list func TestAuditLogHandler_ListByProvider_PaginationBoundaryEdgeCases(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditLogTestDB(t) securityService := services.NewSecurityService(db) defer securityService.Close() @@ -583,7 +576,6 @@ func TestAuditLogHandler_ListByProvider_PaginationBoundaryEdgeCases(t *testing.T // TestAuditLogHandler_List_InvalidDateFormats tests handling of invalid date formats func TestAuditLogHandler_List_InvalidDateFormats(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditLogTestDB(t) securityService := services.NewSecurityService(db) defer securityService.Close() @@ -624,7 +616,6 @@ func TestAuditLogHandler_List_InvalidDateFormats(t *testing.T) { // TestAuditLogHandler_Get_InternalError tests Get when service returns internal error func TestAuditLogHandler_Get_InternalError(t *testing.T) { - gin.SetMode(gin.TestMode) // Create a fresh DB and immediately close it to simulate internal error db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) diff --git a/backend/internal/api/handlers/auth_handler_test.go b/backend/internal/api/handlers/auth_handler_test.go index 9e945e756..bc437280c 100644 --- a/backend/internal/api/handlers/auth_handler_test.go +++ b/backend/internal/api/handlers/auth_handler_test.go @@ -6,7 +6,6 @@ import ( "encoding/json" "net/http" "net/http/httptest" - "os" "testing" "github.com/Wikid82/charon/backend/internal/api/middleware" @@ -45,7 +44,6 @@ func TestAuthHandler_Login(t *testing.T) { _ = user.SetPassword("password123") db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.POST("/login", handler.Login) @@ -65,9 +63,6 @@ func TestAuthHandler_Login(t *testing.T) { } func TestSetSecureCookie_HTTPS_Strict(t *testing.T) { - gin.SetMode(gin.TestMode) - _ = os.Setenv("CHARON_ENV", "production") - defer func() { _ = os.Unsetenv("CHARON_ENV") }() recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) req := httptest.NewRequest("POST", "https://example.com/login", http.NoBody) @@ -83,7 +78,6 @@ func TestSetSecureCookie_HTTPS_Strict(t *testing.T) { func TestSetSecureCookie_HTTP_Lax(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) req := httptest.NewRequest("POST", "http://192.0.2.10/login", http.NoBody) @@ -100,7 +94,6 @@ func TestSetSecureCookie_HTTP_Lax(t *testing.T) { func TestSetSecureCookie_HTTP_Loopback_Insecure(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) req := httptest.NewRequest("POST", "http://127.0.0.1:8080/login", http.NoBody) @@ -118,9 +111,6 @@ func TestSetSecureCookie_HTTP_Loopback_Insecure(t *testing.T) { func TestSetSecureCookie_ForwardedHTTPS_LocalhostForcesInsecure(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) - _ = os.Setenv("CHARON_ENV", "production") - defer func() { _ = os.Unsetenv("CHARON_ENV") }() recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) @@ -139,9 +129,6 @@ func TestSetSecureCookie_ForwardedHTTPS_LocalhostForcesInsecure(t *testing.T) { func TestSetSecureCookie_ForwardedHTTPS_LoopbackForcesInsecure(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) - _ = os.Setenv("CHARON_ENV", "production") - defer func() { _ = os.Unsetenv("CHARON_ENV") }() recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) @@ -160,9 +147,6 @@ func TestSetSecureCookie_ForwardedHTTPS_LoopbackForcesInsecure(t *testing.T) { func TestSetSecureCookie_ForwardedHostLocalhostForcesInsecure(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) - _ = os.Setenv("CHARON_ENV", "production") - defer func() { _ = os.Unsetenv("CHARON_ENV") }() recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) @@ -182,9 +166,6 @@ func TestSetSecureCookie_ForwardedHostLocalhostForcesInsecure(t *testing.T) { func TestSetSecureCookie_OriginLoopbackForcesInsecure(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) - _ = os.Setenv("CHARON_ENV", "production") - defer func() { _ = os.Unsetenv("CHARON_ENV") }() recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) @@ -204,7 +185,6 @@ func TestSetSecureCookie_OriginLoopbackForcesInsecure(t *testing.T) { func TestSetSecureCookie_HTTP_PrivateIP_Insecure(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) req := httptest.NewRequest("POST", "http://192.168.1.50:8080/login", http.NoBody) @@ -222,7 +202,6 @@ func TestSetSecureCookie_HTTP_PrivateIP_Insecure(t *testing.T) { func TestSetSecureCookie_HTTP_10Network_Insecure(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) req := httptest.NewRequest("POST", "http://10.0.0.5:8080/login", http.NoBody) @@ -240,7 +219,6 @@ func TestSetSecureCookie_HTTP_10Network_Insecure(t *testing.T) { func TestSetSecureCookie_HTTP_172Network_Insecure(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) req := httptest.NewRequest("POST", "http://172.16.0.1:8080/login", http.NoBody) @@ -258,7 +236,6 @@ func TestSetSecureCookie_HTTP_172Network_Insecure(t *testing.T) { func TestSetSecureCookie_HTTPS_PrivateIP_Secure(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) req := httptest.NewRequest("POST", "https://192.168.1.50:8080/login", http.NoBody) @@ -276,7 +253,6 @@ func TestSetSecureCookie_HTTPS_PrivateIP_Secure(t *testing.T) { func TestSetSecureCookie_HTTP_IPv6ULA_Insecure(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) req := httptest.NewRequest("POST", "http://[fd12::1]:8080/login", http.NoBody) @@ -294,7 +270,6 @@ func TestSetSecureCookie_HTTP_IPv6ULA_Insecure(t *testing.T) { func TestSetSecureCookie_HTTP_PublicIP_Secure(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) req := httptest.NewRequest("POST", "http://203.0.113.5:8080/login", http.NoBody) @@ -322,7 +297,6 @@ func TestIsProduction(t *testing.T) { } func TestRequestScheme(t *testing.T) { - gin.SetMode(gin.TestMode) t.Run("forwarded proto first value wins", func(t *testing.T) { recorder := httptest.NewRecorder() @@ -393,7 +367,6 @@ func TestHostHelpers(t *testing.T) { } func TestIsLocalRequest(t *testing.T) { - gin.SetMode(gin.TestMode) t.Run("forwarded host list includes localhost", func(t *testing.T) { recorder := httptest.NewRecorder() @@ -428,7 +401,6 @@ func TestIsLocalRequest(t *testing.T) { } func TestClearSecureCookie(t *testing.T) { - gin.SetMode(gin.TestMode) recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) ctx.Request = httptest.NewRequest("POST", "http://example.com/logout", http.NoBody) @@ -445,7 +417,6 @@ func TestClearSecureCookie(t *testing.T) { func TestAuthHandler_Login_Errors(t *testing.T) { t.Parallel() handler, _ := setupAuthHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() r.POST("/login", handler.Login) @@ -473,7 +444,6 @@ func TestAuthHandler_Register(t *testing.T) { t.Parallel() handler, _ := setupAuthHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() r.POST("/register", handler.Register) @@ -497,7 +467,6 @@ func TestAuthHandler_Register_Duplicate(t *testing.T) { handler, db := setupAuthHandler(t) db.Create(&models.User{UUID: uuid.NewString(), Email: "dup@example.com", Name: "Dup"}) - gin.SetMode(gin.TestMode) r := gin.New() r.POST("/register", handler.Register) @@ -519,7 +488,6 @@ func TestAuthHandler_Logout(t *testing.T) { t.Parallel() handler, _ := setupAuthHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() r.POST("/logout", handler.Logout) @@ -548,7 +516,6 @@ func TestAuthHandler_Me(t *testing.T) { } db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() // Simulate middleware r.Use(func(c *gin.Context) { @@ -574,7 +541,6 @@ func TestAuthHandler_Me(t *testing.T) { func TestAuthHandler_Me_NotFound(t *testing.T) { t.Parallel() handler, _ := setupAuthHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("userID", uint(999)) // Non-existent ID @@ -602,7 +568,6 @@ func TestAuthHandler_ChangePassword(t *testing.T) { _ = user.SetPassword("oldpassword") db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() // Simulate middleware r.Use(func(c *gin.Context) { @@ -637,7 +602,6 @@ func TestAuthHandler_ChangePassword_WrongOld(t *testing.T) { _ = user.SetPassword("correct") db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("userID", user.ID) @@ -661,7 +625,6 @@ func TestAuthHandler_ChangePassword_WrongOld(t *testing.T) { func TestAuthHandler_ChangePassword_Errors(t *testing.T) { t.Parallel() handler, _ := setupAuthHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() r.POST("/change-password", handler.ChangePassword) @@ -708,7 +671,6 @@ func TestNewAuthHandlerWithDB(t *testing.T) { func TestAuthHandler_Verify_NoCookie(t *testing.T) { t.Parallel() handler, _ := setupAuthHandlerWithDB(t) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/verify", handler.Verify) @@ -723,7 +685,6 @@ func TestAuthHandler_Verify_NoCookie(t *testing.T) { func TestAuthHandler_Verify_InvalidToken(t *testing.T) { t.Parallel() handler, _ := setupAuthHandlerWithDB(t) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/verify", handler.Verify) @@ -753,7 +714,6 @@ func TestAuthHandler_Verify_ValidToken(t *testing.T) { // Generate token token, _ := handler.authService.GenerateToken(user) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/verify", handler.Verify) @@ -783,7 +743,6 @@ func TestAuthHandler_Verify_BearerToken(t *testing.T) { token, _ := handler.authService.GenerateToken(user) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/verify", handler.Verify) @@ -813,7 +772,6 @@ func TestAuthHandler_Verify_DisabledUser(t *testing.T) { token, _ := handler.authService.GenerateToken(user) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/verify", handler.Verify) @@ -853,7 +811,6 @@ func TestAuthHandler_Verify_ForwardAuthDenied(t *testing.T) { token, _ := handler.authService.GenerateToken(user) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/verify", handler.Verify) @@ -869,7 +826,6 @@ func TestAuthHandler_Verify_ForwardAuthDenied(t *testing.T) { func TestAuthHandler_VerifyStatus_NotAuthenticated(t *testing.T) { t.Parallel() handler, _ := setupAuthHandlerWithDB(t) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/status", handler.VerifyStatus) @@ -886,7 +842,6 @@ func TestAuthHandler_VerifyStatus_NotAuthenticated(t *testing.T) { func TestAuthHandler_VerifyStatus_InvalidToken(t *testing.T) { t.Parallel() handler, _ := setupAuthHandlerWithDB(t) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/status", handler.VerifyStatus) @@ -917,7 +872,6 @@ func TestAuthHandler_VerifyStatus_Authenticated(t *testing.T) { token, _ := handler.authService.GenerateToken(user) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/status", handler.VerifyStatus) @@ -951,7 +905,6 @@ func TestAuthHandler_VerifyStatus_DisabledUser(t *testing.T) { token, _ := handler.authService.GenerateToken(user) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/status", handler.VerifyStatus) @@ -969,7 +922,6 @@ func TestAuthHandler_VerifyStatus_DisabledUser(t *testing.T) { func TestAuthHandler_GetAccessibleHosts_Unauthorized(t *testing.T) { t.Parallel() handler, _ := setupAuthHandlerWithDB(t) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/hosts", handler.GetAccessibleHosts) @@ -1000,7 +952,6 @@ func TestAuthHandler_GetAccessibleHosts_AllowAll(t *testing.T) { } db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("userID", user.ID) @@ -1037,7 +988,6 @@ func TestAuthHandler_GetAccessibleHosts_DenyAll(t *testing.T) { } db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("userID", user.ID) @@ -1077,7 +1027,6 @@ func TestAuthHandler_GetAccessibleHosts_PermittedHosts(t *testing.T) { } db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("userID", user.ID) @@ -1100,7 +1049,6 @@ func TestAuthHandler_GetAccessibleHosts_UserNotFound(t *testing.T) { t.Parallel() handler, _ := setupAuthHandlerWithDB(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("userID", uint(99999)) @@ -1118,7 +1066,6 @@ func TestAuthHandler_GetAccessibleHosts_UserNotFound(t *testing.T) { func TestAuthHandler_CheckHostAccess_Unauthorized(t *testing.T) { t.Parallel() handler, _ := setupAuthHandlerWithDB(t) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/hosts/:hostId/access", handler.CheckHostAccess) @@ -1136,7 +1083,6 @@ func TestAuthHandler_CheckHostAccess_InvalidHostID(t *testing.T) { user := &models.User{UUID: uuid.NewString(), Email: "check@example.com", Enabled: true} db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("userID", user.ID) @@ -1166,7 +1112,6 @@ func TestAuthHandler_CheckHostAccess_Allowed(t *testing.T) { } db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("userID", user.ID) @@ -1199,7 +1144,6 @@ func TestAuthHandler_CheckHostAccess_Denied(t *testing.T) { } db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("userID", user.ID) @@ -1276,7 +1220,6 @@ func TestAuthHandler_Me_RequiresUserContext(t *testing.T) { t.Parallel() handler, _ := setupAuthHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/me", handler.Me) @@ -1360,7 +1303,6 @@ func TestAuthHandler_Refresh(t *testing.T) { require.NoError(t, user.SetPassword("password123")) require.NoError(t, db.Create(user).Error) - gin.SetMode(gin.TestMode) r := gin.New() r.POST("/refresh", func(c *gin.Context) { c.Set("userID", user.ID) @@ -1381,7 +1323,6 @@ func TestAuthHandler_Refresh_Unauthorized(t *testing.T) { t.Parallel() handler, _ := setupAuthHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() r.POST("/refresh", handler.Refresh) @@ -1396,7 +1337,6 @@ func TestAuthHandler_Register_BadRequest(t *testing.T) { t.Parallel() handler, _ := setupAuthHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() r.POST("/register", handler.Register) @@ -1412,7 +1352,6 @@ func TestAuthHandler_Logout_InvalidateSessionsFailure(t *testing.T) { t.Parallel() handler, _ := setupAuthHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("userID", uint(999999)) @@ -1456,7 +1395,6 @@ func TestAuthHandler_Verify_UsesOriginalHostFallback(t *testing.T) { token, err := handler.authService.GenerateToken(user) require.NoError(t, err) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/verify", handler.Verify) @@ -1474,7 +1412,6 @@ func TestAuthHandler_GetAccessibleHosts_DatabaseUnavailable(t *testing.T) { t.Parallel() handler, _ := setupAuthHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("userID", uint(1)) @@ -1494,7 +1431,6 @@ func TestAuthHandler_CheckHostAccess_DatabaseUnavailable(t *testing.T) { t.Parallel() handler, _ := setupAuthHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("userID", uint(1)) @@ -1514,7 +1450,6 @@ func TestAuthHandler_CheckHostAccess_UserNotFound(t *testing.T) { t.Parallel() handler, _ := setupAuthHandlerWithDB(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("userID", uint(999999)) diff --git a/backend/internal/api/handlers/backup_handler_sanitize_test.go b/backend/internal/api/handlers/backup_handler_sanitize_test.go index 2584811a9..c26ab8ec5 100644 --- a/backend/internal/api/handlers/backup_handler_sanitize_test.go +++ b/backend/internal/api/handlers/backup_handler_sanitize_test.go @@ -16,7 +16,6 @@ import ( ) func TestBackupHandlerSanitizesFilename(t *testing.T) { - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() // prepare a fake "database" dbPath := filepath.Join(tmpDir, "db.sqlite") diff --git a/backend/internal/api/handlers/cerberus_logs_ws_test.go b/backend/internal/api/handlers/cerberus_logs_ws_test.go index a6202dff1..e52206148 100644 --- a/backend/internal/api/handlers/cerberus_logs_ws_test.go +++ b/backend/internal/api/handlers/cerberus_logs_ws_test.go @@ -21,7 +21,6 @@ import ( ) func init() { - gin.SetMode(gin.TestMode) } // TestCerberusLogsHandler_NewHandler verifies handler creation. diff --git a/backend/internal/api/handlers/certificate_handler_coverage_test.go b/backend/internal/api/handlers/certificate_handler_coverage_test.go index e936bc00a..acf70e3dd 100644 --- a/backend/internal/api/handlers/certificate_handler_coverage_test.go +++ b/backend/internal/api/handlers/certificate_handler_coverage_test.go @@ -16,7 +16,6 @@ func TestCertificateHandler_List_DBError(t *testing.T) { db := OpenTestDB(t) // Don't migrate to cause error - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -33,7 +32,6 @@ func TestCertificateHandler_List_DBError(t *testing.T) { func TestCertificateHandler_Delete_InvalidID(t *testing.T) { db := OpenTestDBWithMigrations(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -50,7 +48,6 @@ func TestCertificateHandler_Delete_InvalidID(t *testing.T) { func TestCertificateHandler_Delete_NotFound(t *testing.T) { db := OpenTestDBWithMigrations(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -71,7 +68,6 @@ func TestCertificateHandler_Delete_NoBackupService(t *testing.T) { cert := models.SSLCertificate{UUID: "test-cert-no-backup", Name: "no-backup-cert", Provider: "custom", Domains: "nobackup.example.com"} db.Create(&cert) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -97,7 +93,6 @@ func TestCertificateHandler_Delete_CheckUsageDBError(t *testing.T) { cert := models.SSLCertificate{UUID: "test-cert-db-err", Name: "db-error-cert", Provider: "custom", Domains: "dberr.example.com"} db.Create(&cert) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -118,7 +113,6 @@ func TestCertificateHandler_List_WithCertificates(t *testing.T) { db.Create(&models.SSLCertificate{UUID: "cert-1", Name: "Cert 1", Provider: "custom", Domains: "one.example.com"}) db.Create(&models.SSLCertificate{UUID: "cert-2", Name: "Cert 2", Provider: "custom", Domains: "two.example.com"}) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -139,7 +133,6 @@ func TestCertificateHandler_Delete_ZeroID(t *testing.T) { // DELETE /api/certificates/0 should return 400 Bad Request db := OpenTestDBWithMigrations(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -173,7 +166,6 @@ func TestCertificateHandler_DBSetupOrdering(t *testing.T) { t.Fatalf("expected proxy_hosts table to exist before service initialization") } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) diff --git a/backend/internal/api/handlers/certificate_handler_security_test.go b/backend/internal/api/handlers/certificate_handler_security_test.go index 9df3eabb7..a118fa7ff 100644 --- a/backend/internal/api/handlers/certificate_handler_security_test.go +++ b/backend/internal/api/handlers/certificate_handler_security_test.go @@ -25,7 +25,6 @@ func TestCertificateHandler_Delete_RequiresAuth(t *testing.T) { t.Fatalf("failed to migrate: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() // Add a middleware that rejects all unauthenticated requests r.Use(func(c *gin.Context) { @@ -55,7 +54,6 @@ func TestCertificateHandler_List_RequiresAuth(t *testing.T) { t.Fatalf("failed to migrate: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() // Add a middleware that rejects all unauthenticated requests r.Use(func(c *gin.Context) { @@ -85,7 +83,6 @@ func TestCertificateHandler_Upload_RequiresAuth(t *testing.T) { t.Fatalf("failed to migrate: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() // Add a middleware that rejects all unauthenticated requests r.Use(func(c *gin.Context) { @@ -126,7 +123,6 @@ func TestCertificateHandler_Delete_DiskSpaceCheck(t *testing.T) { t.Fatalf("failed to create cert: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -179,7 +175,6 @@ func TestCertificateHandler_Delete_NotificationRateLimiting(t *testing.T) { t.Fatalf("failed to create cert2: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) diff --git a/backend/internal/api/handlers/certificate_handler_test.go b/backend/internal/api/handlers/certificate_handler_test.go index bb10ac016..7971bcbc6 100644 --- a/backend/internal/api/handlers/certificate_handler_test.go +++ b/backend/internal/api/handlers/certificate_handler_test.go @@ -36,7 +36,6 @@ func mockAuthMiddleware() gin.HandlerFunc { func setupCertTestRouter(t *testing.T, db *gorm.DB) *gin.Engine { t.Helper() - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) @@ -110,7 +109,6 @@ func TestDeleteCertificate_CreatesBackup(t *testing.T) { t.Fatalf("failed to create cert: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -164,7 +162,6 @@ func TestDeleteCertificate_BackupFailure(t *testing.T) { t.Fatalf("failed to create cert: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -218,7 +215,6 @@ func TestDeleteCertificate_InUse_NoBackup(t *testing.T) { t.Fatalf("failed to create proxy host: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -296,7 +292,6 @@ func TestCertificateHandler_List(t *testing.T) { t.Fatalf("failed to migrate: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) r.Use(mockAuthMiddleware()) @@ -324,7 +319,6 @@ func TestCertificateHandler_Upload_MissingName(t *testing.T) { t.Fatalf("failed to migrate: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -352,7 +346,6 @@ func TestCertificateHandler_Upload_MissingCertFile(t *testing.T) { t.Fatalf("failed to migrate: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -383,7 +376,6 @@ func TestCertificateHandler_Upload_MissingKeyFile(t *testing.T) { t.Fatalf("failed to migrate: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -410,7 +402,6 @@ func TestCertificateHandler_Upload_MissingKeyFile_MultipartWithCert(t *testing.T t.Fatalf("failed to migrate: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -450,7 +441,6 @@ func TestCertificateHandler_Upload_Success(t *testing.T) { t.Fatalf("failed to migrate: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) @@ -525,7 +515,6 @@ func TestCertificateHandler_Upload_WithNotificationService(t *testing.T) { require.NoError(t, err) require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.Setting{}, &models.NotificationProvider{})) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) @@ -564,7 +553,6 @@ func TestDeleteCertificate_InvalidID(t *testing.T) { t.Fatalf("failed to migrate: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -590,7 +578,6 @@ func TestDeleteCertificate_ZeroID(t *testing.T) { t.Fatalf("failed to migrate: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -622,7 +609,6 @@ func TestDeleteCertificate_LowDiskSpace(t *testing.T) { t.Fatalf("failed to create cert: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -671,7 +657,6 @@ func TestDeleteCertificate_DiskSpaceCheckError(t *testing.T) { t.Fatalf("failed to create cert: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -730,7 +715,6 @@ func TestDeleteCertificate_ExpiredLetsEncrypt_NotInUse(t *testing.T) { t.Fatalf("failed to create cert: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -789,7 +773,6 @@ func TestDeleteCertificate_ValidLetsEncrypt_NotInUse(t *testing.T) { t.Fatalf("failed to create cert: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -835,7 +818,6 @@ func TestDeleteCertificate_UsageCheckError(t *testing.T) { t.Fatalf("failed to create cert: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) @@ -873,7 +855,6 @@ func TestDeleteCertificate_NotificationRateLimit(t *testing.T) { t.Fatalf("failed to create cert2: %v", err) } - gin.SetMode(gin.TestMode) r := gin.New() r.Use(mockAuthMiddleware()) svc := services.NewCertificateService("/tmp", db) diff --git a/backend/internal/api/handlers/coverage_helpers_test.go b/backend/internal/api/handlers/coverage_helpers_test.go index ce6fa7ef8..cde20263f 100644 --- a/backend/internal/api/handlers/coverage_helpers_test.go +++ b/backend/internal/api/handlers/coverage_helpers_test.go @@ -129,7 +129,6 @@ func Test_mapCrowdsecStatus(t *testing.T) { // Test actorFromContext helper function func Test_actorFromContext(t *testing.T) { - gin.SetMode(gin.TestMode) t.Run("with userID in context", func(t *testing.T) { c, _ := gin.CreateTestContext(httptest.NewRecorder()) @@ -157,7 +156,6 @@ func Test_actorFromContext(t *testing.T) { // Test hubEndpoints helper function func Test_hubEndpoints(t *testing.T) { - gin.SetMode(gin.TestMode) t.Run("nil Hub returns nil", func(t *testing.T) { h := &CrowdsecHandler{Hub: nil} @@ -193,7 +191,6 @@ func TestRealCommandExecutor_Execute(t *testing.T) { // Test isCerberusEnabled helper func Test_isCerberusEnabled(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{})) @@ -243,7 +240,6 @@ func Test_isCerberusEnabled(t *testing.T) { // Test isConsoleEnrollmentEnabled helper func Test_isConsoleEnrollmentEnabled(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{})) @@ -293,7 +289,6 @@ func Test_isConsoleEnrollmentEnabled(t *testing.T) { // Test CrowdsecHandler.ExportConfig func TestCrowdsecHandler_ExportConfig(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -320,7 +315,6 @@ func TestCrowdsecHandler_ExportConfig(t *testing.T) { // Test CrowdsecHandler.CheckLAPIHealth func TestCrowdsecHandler_CheckLAPIHealth(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -340,7 +334,6 @@ func TestCrowdsecHandler_CheckLAPIHealth(t *testing.T) { // Test CrowdsecHandler Console endpoints func TestCrowdsecHandler_ConsoleStatus(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{}, &models.CrowdsecConsoleEnrollment{})) @@ -362,7 +355,6 @@ func TestCrowdsecHandler_ConsoleStatus(t *testing.T) { } func TestCrowdsecHandler_ConsoleEnroll_Disabled(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{})) @@ -385,7 +377,6 @@ func TestCrowdsecHandler_ConsoleEnroll_Disabled(t *testing.T) { } func TestCrowdsecHandler_DeleteConsoleEnrollment(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{})) @@ -405,7 +396,6 @@ func TestCrowdsecHandler_DeleteConsoleEnrollment(t *testing.T) { // Test CrowdsecHandler.BanIP and UnbanIP func TestCrowdsecHandler_BanIP(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{})) @@ -434,7 +424,6 @@ func TestCrowdsecHandler_BanIP(t *testing.T) { } func TestCrowdsecHandler_UnbanIP(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{})) @@ -460,7 +449,6 @@ func TestCrowdsecHandler_UnbanIP(t *testing.T) { // Test CrowdsecHandler.UpdateAcquisitionConfig func TestCrowdsecHandler_UpdateAcquisitionConfig(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{})) @@ -540,7 +528,6 @@ func Test_safeFloat64ToUint(t *testing.T) { // Test CrowdsecHandler_DiagnosticsConnectivity func TestCrowdsecHandler_DiagnosticsConnectivity(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{}, &models.CrowdsecConsoleEnrollment{})) @@ -569,7 +556,6 @@ func TestCrowdsecHandler_DiagnosticsConnectivity(t *testing.T) { // Test CrowdsecHandler_DiagnosticsConfig func TestCrowdsecHandler_DiagnosticsConfig(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{})) @@ -595,7 +581,6 @@ func TestCrowdsecHandler_DiagnosticsConfig(t *testing.T) { // Test CrowdsecHandler_ConsoleHeartbeat func TestCrowdsecHandler_ConsoleHeartbeat(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{}, &models.CrowdsecConsoleEnrollment{})) @@ -623,7 +608,6 @@ func TestCrowdsecHandler_ConsoleHeartbeat(t *testing.T) { // Test CrowdsecHandler_ConsoleHeartbeat_Disabled func TestCrowdsecHandler_ConsoleHeartbeat_Disabled(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{})) diff --git a/backend/internal/api/handlers/coverage_quick_test.go b/backend/internal/api/handlers/coverage_quick_test.go index 9bdd66616..e525d3755 100644 --- a/backend/internal/api/handlers/coverage_quick_test.go +++ b/backend/internal/api/handlers/coverage_quick_test.go @@ -33,7 +33,6 @@ func createValidSQLiteDB(t *testing.T, dbPath string) error { // Use a real BackupService, but point it at tmpDir for isolation func TestBackupHandlerQuick(t *testing.T) { - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() // Create a valid SQLite database for backup operations dbPath := filepath.Join(tmpDir, "db.sqlite") diff --git a/backend/internal/api/handlers/credential_handler_test.go b/backend/internal/api/handlers/credential_handler_test.go index 11a2965a8..fee64ebdf 100644 --- a/backend/internal/api/handlers/credential_handler_test.go +++ b/backend/internal/api/handlers/credential_handler_test.go @@ -31,7 +31,6 @@ func setupCredentialHandlerTest(t *testing.T) (*gin.Engine, *gorm.DB, *models.DN _ = os.Unsetenv("CHARON_ENCRYPTION_KEY") }) - gin.SetMode(gin.TestMode) router := gin.New() // Use test name for unique database with WAL mode to avoid locking issues diff --git a/backend/internal/api/handlers/crowdsec_archive_validation_test.go b/backend/internal/api/handlers/crowdsec_archive_validation_test.go index 6ecca4b7f..4e047533a 100644 --- a/backend/internal/api/handlers/crowdsec_archive_validation_test.go +++ b/backend/internal/api/handlers/crowdsec_archive_validation_test.go @@ -251,7 +251,6 @@ func TestConfigArchiveValidator_RequiredFiles(t *testing.T) { // TestImportConfig_Validation tests the enhanced ImportConfig handler with validation. func TestImportConfig_Validation(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := OpenTestDB(t) tmpDir := t.TempDir() @@ -320,7 +319,6 @@ func TestImportConfig_Validation(t *testing.T) { // TestImportConfig_Rollback tests backup restoration on validation failure. func TestImportConfig_Rollback(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := OpenTestDB(t) tmpDir := t.TempDir() diff --git a/backend/internal/api/handlers/crowdsec_cache_verification_test.go b/backend/internal/api/handlers/crowdsec_cache_verification_test.go index 05f870a6c..656148b27 100644 --- a/backend/internal/api/handlers/crowdsec_cache_verification_test.go +++ b/backend/internal/api/handlers/crowdsec_cache_verification_test.go @@ -16,7 +16,6 @@ import ( // TestListPresetsShowsCachedStatus verifies the /presets endpoint marks cached presets. func TestListPresetsShowsCachedStatus(t *testing.T) { - gin.SetMode(gin.TestMode) cacheDir := t.TempDir() dataDir := t.TempDir() diff --git a/backend/internal/api/handlers/crowdsec_coverage_boost_test.go b/backend/internal/api/handlers/crowdsec_coverage_boost_test.go index b5ef3b7cc..faf7ab376 100644 --- a/backend/internal/api/handlers/crowdsec_coverage_boost_test.go +++ b/backend/internal/api/handlers/crowdsec_coverage_boost_test.go @@ -16,7 +16,6 @@ import ( // ============================================ func TestUpdateAcquisitionConfigMissingContent(t *testing.T) { - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() g := r.Group("/api/v1") @@ -33,7 +32,6 @@ func TestUpdateAcquisitionConfigMissingContent(t *testing.T) { } func TestUpdateAcquisitionConfigInvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() g := r.Group("/api/v1") @@ -49,7 +47,6 @@ func TestUpdateAcquisitionConfigInvalidJSON(t *testing.T) { } func TestGetLAPIDecisionsWithIPFilter(t *testing.T) { - gin.SetMode(gin.TestMode) mockExec := &mockCommandExecutor{output: []byte(`[]`), err: nil} h := &CrowdsecHandler{ CmdExec: mockExec, @@ -68,7 +65,6 @@ func TestGetLAPIDecisionsWithIPFilter(t *testing.T) { } func TestGetLAPIDecisionsWithScopeFilter(t *testing.T) { - gin.SetMode(gin.TestMode) mockExec := &mockCommandExecutor{output: []byte(`[]`), err: nil} h := &CrowdsecHandler{ CmdExec: mockExec, @@ -86,7 +82,6 @@ func TestGetLAPIDecisionsWithScopeFilter(t *testing.T) { } func TestGetLAPIDecisionsWithTypeFilter(t *testing.T) { - gin.SetMode(gin.TestMode) mockExec := &mockCommandExecutor{output: []byte(`[]`), err: nil} h := &CrowdsecHandler{ CmdExec: mockExec, @@ -104,7 +99,6 @@ func TestGetLAPIDecisionsWithTypeFilter(t *testing.T) { } func TestGetLAPIDecisionsWithMultipleFilters(t *testing.T) { - gin.SetMode(gin.TestMode) mockExec := &mockCommandExecutor{output: []byte(`[]`), err: nil} h := &CrowdsecHandler{ CmdExec: mockExec, diff --git a/backend/internal/api/handlers/crowdsec_coverage_gap_test.go b/backend/internal/api/handlers/crowdsec_coverage_gap_test.go index ff5c78aa6..38b1cb795 100644 --- a/backend/internal/api/handlers/crowdsec_coverage_gap_test.go +++ b/backend/internal/api/handlers/crowdsec_coverage_gap_test.go @@ -32,7 +32,6 @@ func (m *MockCommandExecutor) ExecuteWithEnv(ctx context.Context, name string, a // TestConsoleEnrollMissingKey covers the "enrollment_key required" branch func TestConsoleEnrollMissingKey(t *testing.T) { - gin.SetMode(gin.TestMode) mockExec := new(MockCommandExecutor) @@ -59,7 +58,6 @@ func TestConsoleEnrollMissingKey(t *testing.T) { // TestGetCachedPreset_ValidationAndMiss covers path param validation empty check (if any) and cache miss func TestGetCachedPreset_ValidationAndMiss(t *testing.T) { - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() cache, _ := crowdsec.NewHubCache(tmpDir, time.Hour) @@ -86,7 +84,6 @@ func TestGetCachedPreset_ValidationAndMiss(t *testing.T) { } func TestGetCachedPreset_SlugRequired(t *testing.T) { - gin.SetMode(gin.TestMode) h := &CrowdsecHandler{} t.Setenv("FEATURE_CERBERUS_ENABLED", "1") diff --git a/backend/internal/api/handlers/crowdsec_coverage_target_test.go b/backend/internal/api/handlers/crowdsec_coverage_target_test.go index 164cc86a8..2a5c5a8e9 100644 --- a/backend/internal/api/handlers/crowdsec_coverage_target_test.go +++ b/backend/internal/api/handlers/crowdsec_coverage_target_test.go @@ -22,7 +22,6 @@ import ( // TestUpdateAcquisitionConfigSuccess tests successful config update func TestUpdateAcquisitionConfigSuccess(t *testing.T) { - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() // Create fake acquis.yaml path in tmp @@ -50,7 +49,6 @@ func TestUpdateAcquisitionConfigSuccess(t *testing.T) { // TestRegisterBouncerScriptPathError tests script not found func TestRegisterBouncerScriptPathError(t *testing.T) { - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() g := r.Group("/api/v1") @@ -92,7 +90,6 @@ func (f *fakeExecWithOutput) Status(ctx context.Context, configDir string) (runn // TestGetLAPIDecisionsRequestError tests request creation error func TestGetLAPIDecisionsEmptyResponse(t *testing.T) { - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() g := r.Group("/api/v1") @@ -109,7 +106,6 @@ func TestGetLAPIDecisionsEmptyResponse(t *testing.T) { // TestGetLAPIDecisionsWithFilters tests query parameter handling func TestGetLAPIDecisionsIPQueryParam(t *testing.T) { - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() g := r.Group("/api/v1") @@ -124,7 +120,6 @@ func TestGetLAPIDecisionsIPQueryParam(t *testing.T) { // TestGetLAPIDecisionsScopeParam tests scope parameter func TestGetLAPIDecisionsScopeParam(t *testing.T) { - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() g := r.Group("/api/v1") @@ -139,7 +134,6 @@ func TestGetLAPIDecisionsScopeParam(t *testing.T) { // TestGetLAPIDecisionsTypeParam tests type parameter func TestGetLAPIDecisionsTypeParam(t *testing.T) { - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() g := r.Group("/api/v1") @@ -154,7 +148,6 @@ func TestGetLAPIDecisionsTypeParam(t *testing.T) { // TestGetLAPIDecisionsCombinedParams tests multiple query params func TestGetLAPIDecisionsCombinedParams(t *testing.T) { - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() g := r.Group("/api/v1") @@ -169,7 +162,6 @@ func TestGetLAPIDecisionsCombinedParams(t *testing.T) { // TestCheckLAPIHealthTimeout tests health check func TestCheckLAPIHealthRequest(t *testing.T) { - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() g := r.Group("/api/v1") @@ -223,7 +215,6 @@ func TestGetLAPIKeyAlternative(t *testing.T) { // TestStatusContextTimeout tests context handling func TestStatusRequest(t *testing.T) { - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() g := r.Group("/api/v1") @@ -238,7 +229,6 @@ func TestStatusRequest(t *testing.T) { // TestRegisterBouncerExecutionSuccess tests successful registration func TestRegisterBouncerFlow(t *testing.T) { - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() // Create fake script @@ -267,7 +257,6 @@ func TestRegisterBouncerFlow(t *testing.T) { // TestRegisterBouncerWithError tests execution error func TestRegisterBouncerExecutionFailure(t *testing.T) { - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() // Create fake script @@ -294,7 +283,6 @@ func TestRegisterBouncerExecutionFailure(t *testing.T) { // TestGetAcquisitionConfigFileError tests file read error func TestGetAcquisitionConfigNotPresent(t *testing.T) { - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() g := r.Group("/api/v1") diff --git a/backend/internal/api/handlers/crowdsec_dashboard.go b/backend/internal/api/handlers/crowdsec_dashboard.go new file mode 100644 index 000000000..ee53f8b61 --- /dev/null +++ b/backend/internal/api/handlers/crowdsec_dashboard.go @@ -0,0 +1,632 @@ +package handlers + +import ( + "context" + "encoding/csv" + "encoding/json" + "fmt" + "math" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/Wikid82/charon/backend/internal/logger" + "github.com/Wikid82/charon/backend/internal/models" + "github.com/Wikid82/charon/backend/internal/network" + "github.com/gin-gonic/gin" +) + +// Cache TTL constants for dashboard endpoints. +const ( + dashSummaryTTL = 30 * time.Second + dashTimelineTTL = 60 * time.Second + dashTopIPsTTL = 60 * time.Second + dashScenariosTTL = 60 * time.Second + dashAlertsTTL = 30 * time.Second + exportMaxRows = 100_000 +) + +// parseTimeRange converts a range string to a start time. Empty string defaults to 24h. +func parseTimeRange(rangeStr string) (time.Time, error) { + now := time.Now().UTC() + switch rangeStr { + case "1h": + return now.Add(-1 * time.Hour), nil + case "6h": + return now.Add(-6 * time.Hour), nil + case "24h", "": + return now.Add(-24 * time.Hour), nil + case "7d": + return now.Add(-7 * 24 * time.Hour), nil + case "30d": + return now.Add(-30 * 24 * time.Hour), nil + default: + return time.Time{}, fmt.Errorf("invalid range: %s (valid: 1h, 6h, 24h, 7d, 30d)", rangeStr) + } +} + +// normalizeRange returns the canonical range string (defaults empty to "24h"). +func normalizeRange(r string) string { + if r == "" { + return "24h" + } + return r +} + +// intervalForRange selects the default time-bucket interval for a given range. +func intervalForRange(rangeStr string) string { + switch rangeStr { + case "1h": + return "5m" + case "6h": + return "15m" + case "24h", "": + return "1h" + case "7d": + return "6h" + case "30d": + return "1d" + default: + return "1h" + } +} + +// intervalToStrftime maps an interval string to the SQLite strftime expression +// used for time bucketing. +func intervalToStrftime(interval string) string { + switch interval { + case "5m": + return "strftime('%Y-%m-%dT%H:', created_at) || printf('%02d:00Z', (CAST(strftime('%M', created_at) AS INTEGER) / 5) * 5)" + case "15m": + return "strftime('%Y-%m-%dT%H:', created_at) || printf('%02d:00Z', (CAST(strftime('%M', created_at) AS INTEGER) / 15) * 15)" + case "1h": + return "strftime('%Y-%m-%dT%H:00:00Z', created_at)" + case "6h": + return "strftime('%Y-%m-%dT', created_at) || printf('%02d:00:00Z', (CAST(strftime('%H', created_at) AS INTEGER) / 6) * 6)" + case "1d": + return "strftime('%Y-%m-%dT00:00:00Z', created_at)" + default: + return "strftime('%Y-%m-%dT%H:00:00Z', created_at)" + } +} + +// validInterval checks whether the provided interval is one of the known values. +func validInterval(interval string) bool { + switch interval { + case "5m", "15m", "1h", "6h", "1d": + return true + default: + return false + } +} + +// sanitizeCSVField prefixes fields starting with formula-trigger characters +// to prevent CSV injection (CWE-1236). +func sanitizeCSVField(field string) string { + if field == "" { + return field + } + switch field[0] { + case '=', '+', '-', '@', '\t', '\r': + return "'" + field + } + return field +} + +// DashboardSummary returns aggregate counts for the dashboard summary cards. +func (h *CrowdsecHandler) DashboardSummary(c *gin.Context) { + rangeStr := normalizeRange(c.Query("range")) + cacheKey := "dashboard:summary:" + rangeStr + + if cached, ok := h.dashCache.Get(cacheKey); ok { + c.JSON(http.StatusOK, cached) + return + } + + since, err := parseTimeRange(rangeStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Historical metrics from SQLite + var totalDecisions int64 + h.DB.Model(&models.SecurityDecision{}). + Where("source = ? AND created_at >= ?", "crowdsec", since). + Count(&totalDecisions) + + var uniqueIPs int64 + h.DB.Model(&models.SecurityDecision{}). + Where("source = ? AND created_at >= ?", "crowdsec", since). + Distinct("ip").Count(&uniqueIPs) + + var topScenario struct { + Scenario string + Cnt int64 + } + h.DB.Model(&models.SecurityDecision{}). + Select("scenario, COUNT(*) as cnt"). + Where("source = ? AND created_at >= ? AND scenario != ''", "crowdsec", since). + Group("scenario"). + Order("cnt DESC"). + Limit(1). + Scan(&topScenario) + + // Trend calculation: compare current period vs previous equal-length period + duration := time.Since(since) + previousSince := since.Add(-duration) + var previousCount int64 + h.DB.Model(&models.SecurityDecision{}). + Where("source = ? AND created_at >= ? AND created_at < ?", "crowdsec", previousSince, since). + Count(&previousCount) + + // Trend: percentage change vs. the previous equal-length period. + // Formula: round((current - previous) / previous * 100, 1) + // Special cases: no previous data → 0; no current data → -100%. + var trend float64 + if previousCount == 0 { + trend = 0.0 + } else if totalDecisions == 0 && previousCount > 0 { + trend = -100.0 + } else { + trend = math.Round(float64(totalDecisions-previousCount)/float64(previousCount)*1000) / 10 + } + + // Active decisions from LAPI (real-time) + activeDecisions := h.fetchActiveDecisionCount(c.Request.Context()) + + result := gin.H{ + "total_decisions": totalDecisions, + "active_decisions": activeDecisions, + "unique_ips": uniqueIPs, + "top_scenario": topScenario.Scenario, + "decisions_trend": trend, + "range": rangeStr, + "cached": false, + "generated_at": time.Now().UTC().Format(time.RFC3339), + } + + h.dashCache.Set(cacheKey, result, dashSummaryTTL) + c.JSON(http.StatusOK, result) +} + +// fetchActiveDecisionCount queries LAPI for active decisions count. +// Returns -1 when LAPI is unreachable. +func (h *CrowdsecHandler) fetchActiveDecisionCount(ctx context.Context) int64 { + lapiURL := "http://127.0.0.1:8085" + if h.Security != nil { + cfg, err := h.Security.Get() + if err == nil && cfg != nil && cfg.CrowdSecAPIURL != "" { + lapiURL = cfg.CrowdSecAPIURL + } + } + + baseURL, err := h.resolveLAPIURLValidator(lapiURL) + if err != nil { + return -1 + } + + endpoint := baseURL.ResolveReference(&url.URL{Path: "/v1/decisions"}) + reqURL := endpoint.String() + + apiKey := getLAPIKey() + + reqCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, reqURL, http.NoBody) + if err != nil { + return -1 + } + if apiKey != "" { + req.Header.Set("X-Api-Key", apiKey) + } + req.Header.Set("Accept", "application/json") + + client := network.NewInternalServiceHTTPClient(10 * time.Second) + resp, err := client.Do(req) + if err != nil { + return -1 + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return -1 + } + + var decisions []interface{} + if decErr := json.NewDecoder(resp.Body).Decode(&decisions); decErr != nil { + return -1 + } + return int64(len(decisions)) +} + +// DashboardTimeline returns time-bucketed decision counts for the timeline chart. +func (h *CrowdsecHandler) DashboardTimeline(c *gin.Context) { + rangeStr := normalizeRange(c.Query("range")) + interval := c.Query("interval") + if interval == "" { + interval = intervalForRange(rangeStr) + } + if !validInterval(interval) { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid interval: %s (valid: 5m, 15m, 1h, 6h, 1d)", interval)}) + return + } + + cacheKey := fmt.Sprintf("dashboard:timeline:%s:%s", rangeStr, interval) + if cached, ok := h.dashCache.Get(cacheKey); ok { + c.JSON(http.StatusOK, cached) + return + } + + since, err := parseTimeRange(rangeStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + bucketExpr := intervalToStrftime(interval) + + type bucketRow struct { + Bucket string + Bans int64 + Captchas int64 + } + var rows []bucketRow + + h.DB.Model(&models.SecurityDecision{}). + Select(fmt.Sprintf("(%s) as bucket, SUM(CASE WHEN action = 'block' THEN 1 ELSE 0 END) as bans, SUM(CASE WHEN action = 'challenge' THEN 1 ELSE 0 END) as captchas", bucketExpr)). + Where("source = ? AND created_at >= ?", "crowdsec", since). + Group("bucket"). + Order("bucket ASC"). + Scan(&rows) + + buckets := make([]gin.H, 0, len(rows)) + for _, r := range rows { + buckets = append(buckets, gin.H{ + "timestamp": r.Bucket, + "bans": r.Bans, + "captchas": r.Captchas, + }) + } + + result := gin.H{ + "buckets": buckets, + "range": rangeStr, + "interval": interval, + "cached": false, + } + + h.dashCache.Set(cacheKey, result, dashTimelineTTL) + c.JSON(http.StatusOK, result) +} + +// DashboardTopIPs returns top attacking IPs ranked by decision count. +func (h *CrowdsecHandler) DashboardTopIPs(c *gin.Context) { + rangeStr := normalizeRange(c.Query("range")) + limitStr := c.DefaultQuery("limit", "10") + limit, err := strconv.Atoi(limitStr) + if err != nil || limit < 1 { + limit = 10 + } + if limit > 50 { + limit = 50 + } + + cacheKey := fmt.Sprintf("dashboard:top-ips:%s:%d", rangeStr, limit) + if cached, ok := h.dashCache.Get(cacheKey); ok { + c.JSON(http.StatusOK, cached) + return + } + + since, err := parseTimeRange(rangeStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + type ipRow struct { + IP string + Count int64 + LastSeen time.Time + Country string + } + var rows []ipRow + + h.DB.Model(&models.SecurityDecision{}). + Select("ip, COUNT(*) as count, MAX(created_at) as last_seen, MAX(country) as country"). + Where("source = ? AND created_at >= ?", "crowdsec", since). + Group("ip"). + Order("count DESC"). + Limit(limit). + Scan(&rows) + + ips := make([]gin.H, 0, len(rows)) + for _, r := range rows { + ips = append(ips, gin.H{ + "ip": r.IP, + "count": r.Count, + "last_seen": r.LastSeen.UTC().Format(time.RFC3339), + "country": r.Country, + }) + } + + result := gin.H{ + "ips": ips, + "range": rangeStr, + "cached": false, + } + + h.dashCache.Set(cacheKey, result, dashTopIPsTTL) + c.JSON(http.StatusOK, result) +} + +// DashboardScenarios returns scenario breakdown with counts and percentages. +func (h *CrowdsecHandler) DashboardScenarios(c *gin.Context) { + rangeStr := normalizeRange(c.Query("range")) + + cacheKey := "dashboard:scenarios:" + rangeStr + if cached, ok := h.dashCache.Get(cacheKey); ok { + c.JSON(http.StatusOK, cached) + return + } + + since, err := parseTimeRange(rangeStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + type scenarioRow struct { + Name string + Count int64 + } + var rows []scenarioRow + + h.DB.Model(&models.SecurityDecision{}). + Select("scenario as name, COUNT(*) as count"). + Where("source = ? AND created_at >= ? AND scenario != ''", "crowdsec", since). + Group("scenario"). + Order("count DESC"). + Limit(50). + Scan(&rows) + + var total int64 + for _, r := range rows { + total += r.Count + } + + scenarios := make([]gin.H, 0, len(rows)) + for _, r := range rows { + pct := 0.0 + if total > 0 { + pct = math.Round(float64(r.Count)/float64(total)*1000) / 10 + } + scenarios = append(scenarios, gin.H{ + "name": r.Name, + "count": r.Count, + "percentage": pct, + }) + } + + result := gin.H{ + "scenarios": scenarios, + "total": total, + "range": rangeStr, + "cached": false, + } + + h.dashCache.Set(cacheKey, result, dashScenariosTTL) + c.JSON(http.StatusOK, result) +} + +// ListAlerts wraps the CrowdSec LAPI /v1/alerts endpoint. +func (h *CrowdsecHandler) ListAlerts(c *gin.Context) { + rangeStr := normalizeRange(c.Query("range")) + scenario := strings.TrimSpace(c.Query("scenario")) + limitStr := c.DefaultQuery("limit", "50") + offsetStr := c.DefaultQuery("offset", "0") + + limit, err := strconv.Atoi(limitStr) + if err != nil || limit < 1 { + limit = 50 + } + if limit > 200 { + limit = 200 + } + offset, err := strconv.Atoi(offsetStr) + if err != nil || offset < 0 { + offset = 0 + } + + cacheKey := fmt.Sprintf("dashboard:alerts:%s:%s:%d:%d", rangeStr, scenario, limit, offset) + if cached, ok := h.dashCache.Get(cacheKey); ok { + c.JSON(http.StatusOK, cached) + return + } + + since, tErr := parseTimeRange(rangeStr) + if tErr != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": tErr.Error()}) + return + } + + alerts, total, source := h.fetchLAPIAlerts(c.Request.Context(), since, scenario, limit, offset) + + result := gin.H{ + "alerts": alerts, + "total": total, + "source": source, + "cached": false, + } + + h.dashCache.Set(cacheKey, result, dashAlertsTTL) + c.JSON(http.StatusOK, result) +} + +// fetchLAPIAlerts attempts to get alerts from LAPI, falling back to cscli. +func (h *CrowdsecHandler) fetchLAPIAlerts(ctx context.Context, since time.Time, scenario string, limit, offset int) (alerts []interface{}, total int, source string) { + lapiURL := "http://127.0.0.1:8085" + if h.Security != nil { + cfg, err := h.Security.Get() + if err == nil && cfg != nil && cfg.CrowdSecAPIURL != "" { + lapiURL = cfg.CrowdSecAPIURL + } + } + + baseURL, err := h.resolveLAPIURLValidator(lapiURL) + if err != nil { + return h.fetchAlertsCscli(ctx, scenario, limit) + } + + q := url.Values{} + q.Set("since", since.Format(time.RFC3339)) + if scenario != "" { + q.Set("scenario", scenario) + } + q.Set("limit", strconv.Itoa(limit)) + + endpoint := baseURL.ResolveReference(&url.URL{Path: "/v1/alerts"}) + endpoint.RawQuery = q.Encode() + reqURL := endpoint.String() + + apiKey := getLAPIKey() + + reqCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + req, reqErr := http.NewRequestWithContext(reqCtx, http.MethodGet, reqURL, http.NoBody) + if reqErr != nil { + return h.fetchAlertsCscli(ctx, scenario, limit) + } + if apiKey != "" { + req.Header.Set("X-Api-Key", apiKey) + } + req.Header.Set("Accept", "application/json") + + client := network.NewInternalServiceHTTPClient(10 * time.Second) + resp, doErr := client.Do(req) + if doErr != nil { + return h.fetchAlertsCscli(ctx, scenario, limit) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return h.fetchAlertsCscli(ctx, scenario, limit) + } + + var rawAlerts []interface{} + if decErr := json.NewDecoder(resp.Body).Decode(&rawAlerts); decErr != nil { + return h.fetchAlertsCscli(ctx, scenario, limit) + } + + // Capture full count before slicing for correct pagination semantics + fullTotal := len(rawAlerts) + + // Apply offset for pagination + if offset > 0 && offset < len(rawAlerts) { + rawAlerts = rawAlerts[offset:] + } else if offset >= len(rawAlerts) { + rawAlerts = nil + } + + if limit < len(rawAlerts) { + rawAlerts = rawAlerts[:limit] + } + + return rawAlerts, fullTotal, "lapi" +} + +// fetchAlertsCscli falls back to using cscli to list alerts. +func (h *CrowdsecHandler) fetchAlertsCscli(ctx context.Context, scenario string, limit int) (alerts []interface{}, total int, source string) { + args := []string{"alerts", "list", "-o", "json"} + if scenario != "" { + args = append(args, "-s", scenario) + } + args = append(args, "-l", strconv.Itoa(limit)) + + output, err := h.CmdExec.Execute(ctx, "cscli", args...) + if err != nil { + logger.Log().WithError(err).Warn("Failed to list alerts via cscli") + return []interface{}{}, 0, "cscli" + } + + if jErr := json.Unmarshal(output, &alerts); jErr != nil { + return []interface{}{}, 0, "cscli" + } + return alerts, len(alerts), "cscli" +} + +// ExportDecisions exports decisions as downloadable CSV or JSON. +func (h *CrowdsecHandler) ExportDecisions(c *gin.Context) { + format := strings.ToLower(c.DefaultQuery("format", "csv")) + rangeStr := normalizeRange(c.Query("range")) + source := strings.ToLower(c.DefaultQuery("source", "all")) + + if format != "csv" && format != "json" { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid format: must be csv or json"}) + return + } + + validSources := map[string]bool{"crowdsec": true, "waf": true, "ratelimit": true, "manual": true, "all": true} + if !validSources[source] { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid source: must be crowdsec, waf, ratelimit, manual, or all"}) + return + } + + since, err := parseTimeRange(rangeStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var decisions []models.SecurityDecision + q := h.DB.Where("created_at >= ?", since) + if source != "all" { + q = q.Where("source = ?", source) + } + q.Order("created_at DESC").Limit(exportMaxRows).Find(&decisions) + + ts := time.Now().UTC().Format("20060102-150405") + + switch format { + case "csv": + filename := fmt.Sprintf("crowdsec-decisions-%s.csv", ts) + c.Header("Content-Type", "text/csv; charset=utf-8") + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) + + w := csv.NewWriter(c.Writer) + _ = w.Write([]string{"uuid", "ip", "action", "source", "scenario", "rule_id", "host", "country", "created_at", "expires_at"}) + for _, d := range decisions { + _ = w.Write([]string{ + d.UUID, + sanitizeCSVField(d.IP), + d.Action, + d.Source, + sanitizeCSVField(d.Scenario), + sanitizeCSVField(d.RuleID), + sanitizeCSVField(d.Host), + sanitizeCSVField(d.Country), + d.CreatedAt.UTC().Format(time.RFC3339), + func() string { + if d.ExpiresAt != nil { + return d.ExpiresAt.UTC().Format(time.RFC3339) + } + return "" + }(), + }) + } + w.Flush() + if err := w.Error(); err != nil { + logger.Log().WithError(err).Warn("CSV export write error") + } + + case "json": + filename := fmt.Sprintf("crowdsec-decisions-%s.json", ts) + c.Header("Content-Type", "application/json") + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) + c.JSON(http.StatusOK, decisions) + } +} diff --git a/backend/internal/api/handlers/crowdsec_dashboard_cache.go b/backend/internal/api/handlers/crowdsec_dashboard_cache.go new file mode 100644 index 000000000..1f738193f --- /dev/null +++ b/backend/internal/api/handlers/crowdsec_dashboard_cache.go @@ -0,0 +1,70 @@ +package handlers + +import ( + "strings" + "sync" + "time" +) + +type cacheEntry struct { + data interface{} + expiresAt time.Time +} + +type dashboardCache struct { + mu sync.RWMutex + entries map[string]*cacheEntry +} + +func newDashboardCache() *dashboardCache { + return &dashboardCache{ + entries: make(map[string]*cacheEntry), + } +} + +func (c *dashboardCache) Get(key string) (interface{}, bool) { + c.mu.RLock() + entry, ok := c.entries[key] + if !ok { + c.mu.RUnlock() + return nil, false + } + if time.Now().Before(entry.expiresAt) { + data := entry.data + c.mu.RUnlock() + return data, true + } + c.mu.RUnlock() + + c.mu.Lock() + defer c.mu.Unlock() + entry, ok = c.entries[key] + if ok && time.Now().After(entry.expiresAt) { + delete(c.entries, key) + } + return nil, false +} + +func (c *dashboardCache) Set(key string, data interface{}, ttl time.Duration) { + c.mu.Lock() + defer c.mu.Unlock() + + c.entries[key] = &cacheEntry{ + data: data, + expiresAt: time.Now().Add(ttl), + } +} + +func (c *dashboardCache) Invalidate(prefixes ...string) { + c.mu.Lock() + defer c.mu.Unlock() + + for key := range c.entries { + for _, prefix := range prefixes { + if strings.HasPrefix(key, prefix) { + delete(c.entries, key) + break + } + } + } +} diff --git a/backend/internal/api/handlers/crowdsec_dashboard_test.go b/backend/internal/api/handlers/crowdsec_dashboard_test.go new file mode 100644 index 000000000..435b77a00 --- /dev/null +++ b/backend/internal/api/handlers/crowdsec_dashboard_test.go @@ -0,0 +1,1310 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/Wikid82/charon/backend/internal/models" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// setupDashboardHandler creates a CrowdsecHandler with an in-memory DB seeded with decisions. +func setupDashboardHandler(t *testing.T) (*CrowdsecHandler, *gin.Engine) { + t.Helper() + + db := OpenTestDB(t) + require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{})) + + h := &CrowdsecHandler{ + DB: db, + Executor: &fakeExec{}, + CmdExec: &fastCmdExec{}, + BinPath: "/bin/false", + DataDir: t.TempDir(), + dashCache: newDashboardCache(), + } + + seedDashboardData(t, h) + + r := gin.New() + g := r.Group("/api/v1") + h.RegisterRoutes(g) + return h, r +} + +// seedDashboardData inserts representative records for testing. +func seedDashboardData(t *testing.T, h *CrowdsecHandler) { + t.Helper() + now := time.Now().UTC() + + decisions := []models.SecurityDecision{ + {UUID: uuid.NewString(), Source: "crowdsec", Action: "block", IP: "10.0.0.1", Scenario: "crowdsecurity/http-probing", Country: "US", CreatedAt: now.Add(-1 * time.Hour)}, + {UUID: uuid.NewString(), Source: "crowdsec", Action: "block", IP: "10.0.0.1", Scenario: "crowdsecurity/http-probing", Country: "US", CreatedAt: now.Add(-2 * time.Hour)}, + {UUID: uuid.NewString(), Source: "crowdsec", Action: "challenge", IP: "10.0.0.2", Scenario: "crowdsecurity/ssh-bf", Country: "DE", CreatedAt: now.Add(-3 * time.Hour)}, + {UUID: uuid.NewString(), Source: "crowdsec", Action: "block", IP: "10.0.0.3", Scenario: "crowdsecurity/http-probing", Country: "FR", CreatedAt: now.Add(-5 * time.Hour)}, + {UUID: uuid.NewString(), Source: "crowdsec", Action: "block", IP: "10.0.0.4", Scenario: "crowdsecurity/http-bad-user-agent", Country: "", CreatedAt: now.Add(-10 * time.Hour)}, + // Old record outside 24h but within 7d + {UUID: uuid.NewString(), Source: "crowdsec", Action: "block", IP: "10.0.0.5", Scenario: "crowdsecurity/http-probing", Country: "JP", CreatedAt: now.Add(-48 * time.Hour)}, + // Non-crowdsec source + {UUID: uuid.NewString(), Source: "waf", Action: "block", IP: "10.0.0.99", Scenario: "waf-rule", Country: "CN", CreatedAt: now.Add(-1 * time.Hour)}, + } + + for _, d := range decisions { + require.NoError(t, h.DB.Create(&d).Error) + } +} + +func TestParseTimeRange(t *testing.T) { + t.Parallel() + tests := []struct { + input string + valid bool + }{ + {"1h", true}, + {"6h", true}, + {"24h", true}, + {"7d", true}, + {"30d", true}, + {"", true}, + {"2h", false}, + {"1w", false}, + {"invalid", false}, + } + + for _, tc := range tests { + t.Run(fmt.Sprintf("range_%s", tc.input), func(t *testing.T) { + _, err := parseTimeRange(tc.input) + if tc.valid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} + +func TestParseTimeRange_DefaultEmpty(t *testing.T) { + t.Parallel() + result, err := parseTimeRange("") + require.NoError(t, err) + expected := time.Now().UTC().Add(-24 * time.Hour) + assert.InDelta(t, expected.UnixMilli(), result.UnixMilli(), 1000) +} + +func TestDashboardSummary_OK(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=24h", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + + assert.Contains(t, body, "total_decisions") + assert.Contains(t, body, "active_decisions") + assert.Contains(t, body, "unique_ips") + assert.Contains(t, body, "top_scenario") + assert.Contains(t, body, "decisions_trend") + assert.Contains(t, body, "range") + assert.Contains(t, body, "generated_at") + assert.Equal(t, "24h", body["range"]) + + // 5 crowdsec decisions within 24h (excludes 48h-old one) + total := body["total_decisions"].(float64) + assert.Equal(t, float64(5), total) + + // 4 unique crowdsec IPs within 24h + assert.Equal(t, float64(4), body["unique_ips"].(float64)) + + // LAPI unreachable in test => -1 + assert.Equal(t, float64(-1), body["active_decisions"].(float64)) +} + +func TestDashboardSummary_InvalidRange(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=99z", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestDashboardSummary_Cached(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + // First call populates cache + w1 := httptest.NewRecorder() + req1 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=24h", http.NoBody) + r.ServeHTTP(w1, req1) + assert.Equal(t, http.StatusOK, w1.Code) + + // Second call should hit cache + w2 := httptest.NewRecorder() + req2 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=24h", http.NoBody) + r.ServeHTTP(w2, req2) + assert.Equal(t, http.StatusOK, w2.Code) +} + +func TestDashboardTimeline_OK(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/timeline?range=24h", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + + assert.Contains(t, body, "buckets") + assert.Contains(t, body, "interval") + assert.Equal(t, "1h", body["interval"]) + assert.Equal(t, "24h", body["range"]) +} + +func TestDashboardTimeline_CustomInterval(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/timeline?range=6h&interval=15m", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + assert.Equal(t, "15m", body["interval"]) +} + +func TestDashboardTimeline_InvalidInterval(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/timeline?interval=99m", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestDashboardTopIPs_OK(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/top-ips?range=24h&limit=3", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + + ips := body["ips"].([]interface{}) + assert.LessOrEqual(t, len(ips), 3) + // 10.0.0.1 has 2 hits, should be first + if len(ips) > 0 { + first := ips[0].(map[string]interface{}) + assert.Equal(t, "10.0.0.1", first["ip"]) + assert.Equal(t, float64(2), first["count"]) + } +} + +func TestDashboardTopIPs_LimitCap(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + // Limit > 50 should be capped + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/top-ips?limit=100", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestDashboardScenarios_OK(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/scenarios?range=24h", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + + assert.Contains(t, body, "scenarios") + assert.Contains(t, body, "total") + scenarios := body["scenarios"].([]interface{}) + assert.Greater(t, len(scenarios), 0) + + // Verify percentages sum to ~100 + var totalPct float64 + for _, s := range scenarios { + sc := s.(map[string]interface{}) + totalPct += sc["percentage"].(float64) + assert.Contains(t, sc, "name") + assert.Contains(t, sc, "count") + } + assert.InDelta(t, 100.0, totalPct, 1.0) +} + +func TestListAlerts_OK(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + + assert.Contains(t, body, "alerts") + assert.Contains(t, body, "source") + // Falls back to cscli which returns empty/error in test + assert.Equal(t, "cscli", body["source"]) +} + +func TestListAlerts_InvalidRange(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=invalid", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestExportDecisions_CSV(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/export?format=csv&range=24h", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Header().Get("Content-Type"), "text/csv") + assert.Contains(t, w.Header().Get("Content-Disposition"), "attachment") + assert.Contains(t, w.Body.String(), "uuid,ip,action,source,scenario") +} + +func TestExportDecisions_JSON(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/export?format=json&range=24h", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Header().Get("Content-Type"), "application/json") + + var decisions []models.SecurityDecision + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &decisions)) + assert.Greater(t, len(decisions), 0) +} + +func TestExportDecisions_InvalidFormat(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/export?format=xml", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestExportDecisions_InvalidSource(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/export?source=evil", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestSanitizeCSVField(t *testing.T) { + t.Parallel() + tests := []struct { + input string + expected string + }{ + {"normal", "normal"}, + {"=cmd", "'=cmd"}, + {"+cmd", "'+cmd"}, + {"-cmd", "'-cmd"}, + {"@cmd", "'@cmd"}, + {"\tcmd", "'\tcmd"}, + {"\rcmd", "'\rcmd"}, + {"", ""}, + } + + for _, tc := range tests { + assert.Equal(t, tc.expected, sanitizeCSVField(tc.input)) + } +} + +func TestDashboardCache_Invalidate(t *testing.T) { + t.Parallel() + cache := newDashboardCache() + cache.Set("dashboard:summary:24h", "data1", 5*time.Minute) + cache.Set("dashboard:timeline:24h", "data2", 5*time.Minute) + cache.Set("other:key", "data3", 5*time.Minute) + + cache.Invalidate("dashboard") + + _, ok1 := cache.Get("dashboard:summary:24h") + assert.False(t, ok1) + + _, ok2 := cache.Get("dashboard:timeline:24h") + assert.False(t, ok2) + + _, ok3 := cache.Get("other:key") + assert.True(t, ok3) +} + +func TestDashboardCache_TTLExpiry(t *testing.T) { + t.Parallel() + cache := newDashboardCache() + cache.Set("key", "value", 1*time.Millisecond) + + time.Sleep(5 * time.Millisecond) + _, ok := cache.Get("key") + assert.False(t, ok) +} + +func TestDashboardCache_TTLExpiry_DeletesEntry(t *testing.T) { + t.Parallel() + cache := newDashboardCache() + cache.Set("expired", "data", 1*time.Millisecond) + + time.Sleep(5 * time.Millisecond) + _, ok := cache.Get("expired") + assert.False(t, ok) + + cache.mu.Lock() + _, stillPresent := cache.entries["expired"] + cache.mu.Unlock() + assert.False(t, stillPresent, "expired entry should be deleted from map") +} + +func TestDashboardSummary_DecisionsTrend(t *testing.T) { + t.Parallel() + + db := OpenTestDB(t) + require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{})) + + h := &CrowdsecHandler{ + DB: db, + Executor: &fakeExec{}, + CmdExec: &fastCmdExec{}, + BinPath: "/bin/false", + DataDir: t.TempDir(), + dashCache: newDashboardCache(), + } + + now := time.Now().UTC() + // Seed 3 decisions in the current 1h period + for i := 0; i < 3; i++ { + require.NoError(t, db.Create(&models.SecurityDecision{ + UUID: uuid.NewString(), Source: "crowdsec", Action: "block", + IP: "192.168.1.1", Scenario: "crowdsecurity/http-probing", + CreatedAt: now.Add(-time.Duration(i+1) * time.Minute), + }).Error) + } + // Seed 2 decisions in the previous 1h period + for i := 0; i < 2; i++ { + require.NoError(t, db.Create(&models.SecurityDecision{ + UUID: uuid.NewString(), Source: "crowdsec", Action: "block", + IP: "192.168.1.2", Scenario: "crowdsecurity/http-probing", + CreatedAt: now.Add(-1*time.Hour - time.Duration(i+1)*time.Minute), + }).Error) + } + + r := gin.New() + g := r.Group("/api/v1") + h.RegisterRoutes(g) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=1h", http.NoBody) + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + + // (3 - 2) / 2 * 100 = 50.0 + trend := body["decisions_trend"].(float64) + assert.InDelta(t, 50.0, trend, 0.1) +} + +func TestExportDecisions_SourceFilter(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/export?format=json&range=7d&source=waf", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var decisions []models.SecurityDecision + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &decisions)) + for _, d := range decisions { + assert.Equal(t, "waf", d.Source) + } +} + +// --------------------------------------------------------------------------- +// Helper functions unit tests +// --------------------------------------------------------------------------- + +func TestNormalizeRange(t *testing.T) { + t.Parallel() + assert.Equal(t, "24h", normalizeRange("")) + assert.Equal(t, "1h", normalizeRange("1h")) + assert.Equal(t, "7d", normalizeRange("7d")) + assert.Equal(t, "30d", normalizeRange("30d")) +} + +func TestIntervalForRange_AllBranches(t *testing.T) { + t.Parallel() + tests := []struct { + rangeStr string + expected string + }{ + {"1h", "5m"}, + {"6h", "15m"}, + {"24h", "1h"}, + {"", "1h"}, + {"7d", "6h"}, + {"30d", "1d"}, + {"unknown", "1h"}, + } + for _, tc := range tests { + assert.Equal(t, tc.expected, intervalForRange(tc.rangeStr), "intervalForRange(%q)", tc.rangeStr) + } +} + +func TestIntervalToStrftime_AllBranches(t *testing.T) { + t.Parallel() + tests := []struct { + interval string + contains string + }{ + {"5m", "/ 5) * 5"}, + {"15m", "/ 15) * 15"}, + {"1h", "%H:00:00Z"}, + {"6h", "/ 6) * 6"}, + {"1d", "T00:00:00Z"}, + {"unknown", "%H:00:00Z"}, + } + for _, tc := range tests { + result := intervalToStrftime(tc.interval) + assert.Contains(t, result, tc.contains, "intervalToStrftime(%q)", tc.interval) + } +} + +func TestValidInterval_AllBranches(t *testing.T) { + t.Parallel() + for _, v := range []string{"5m", "15m", "1h", "6h", "1d"} { + assert.True(t, validInterval(v), "validInterval(%q)", v) + } + assert.False(t, validInterval("10m")) + assert.False(t, validInterval("")) +} + +// --------------------------------------------------------------------------- +// DashboardSummary edge cases +// --------------------------------------------------------------------------- + +func TestDashboardSummary_EmptyRange(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + assert.Equal(t, "24h", body["range"]) +} + +func TestDashboardSummary_7dRange(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=7d", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + assert.Equal(t, "7d", body["range"]) + // 7d includes the 48h-old record + total := body["total_decisions"].(float64) + assert.Equal(t, float64(6), total) +} + +func TestDashboardSummary_TrendNegative100(t *testing.T) { + t.Parallel() + + db := OpenTestDB(t) + require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{})) + + h := &CrowdsecHandler{ + DB: db, + Executor: &fakeExec{}, + CmdExec: &fastCmdExec{}, + BinPath: "/bin/false", + DataDir: t.TempDir(), + dashCache: newDashboardCache(), + } + + now := time.Now().UTC() + // Only seed decisions in the PREVIOUS 1h period (nothing in current) + for i := 0; i < 3; i++ { + require.NoError(t, db.Create(&models.SecurityDecision{ + UUID: uuid.NewString(), Source: "crowdsec", Action: "block", + IP: "192.168.1.1", Scenario: "crowdsecurity/http-probing", + CreatedAt: now.Add(-1*time.Hour - time.Duration(i+1)*time.Minute), + }).Error) + } + + r := gin.New() + g := r.Group("/api/v1") + h.RegisterRoutes(g) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=1h", http.NoBody) + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + assert.Equal(t, -100.0, body["decisions_trend"]) +} + +// --------------------------------------------------------------------------- +// DashboardTimeline edge cases +// --------------------------------------------------------------------------- + +func TestDashboardTimeline_Cached(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + // First call populates cache + w1 := httptest.NewRecorder() + req1 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/timeline?range=24h", http.NoBody) + r.ServeHTTP(w1, req1) + assert.Equal(t, http.StatusOK, w1.Code) + + // Second call hits cache + w2 := httptest.NewRecorder() + req2 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/timeline?range=24h", http.NoBody) + r.ServeHTTP(w2, req2) + assert.Equal(t, http.StatusOK, w2.Code) +} + +func TestDashboardTimeline_InvalidRange(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/timeline?range=99z&interval=1h", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestDashboardTimeline_AllRangeIntervals(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + ranges := []struct { + rangeStr string + interval string + }{ + {"1h", "5m"}, + {"6h", "15m"}, + {"7d", "6h"}, + {"30d", "1d"}, + } + + for _, tc := range ranges { + t.Run(tc.rangeStr, func(t *testing.T) { + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, + fmt.Sprintf("/api/v1/admin/crowdsec/dashboard/timeline?range=%s", tc.rangeStr), http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + assert.Equal(t, tc.interval, body["interval"]) + }) + } +} + +// --------------------------------------------------------------------------- +// DashboardTopIPs edge cases +// --------------------------------------------------------------------------- + +func TestDashboardTopIPs_InvalidRange(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/top-ips?range=bad", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestDashboardTopIPs_BadLimit(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/top-ips?limit=abc", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestDashboardTopIPs_NegativeLimit(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/top-ips?limit=-5", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestDashboardTopIPs_Cached(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w1 := httptest.NewRecorder() + req1 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/top-ips?range=24h&limit=10", http.NoBody) + r.ServeHTTP(w1, req1) + assert.Equal(t, http.StatusOK, w1.Code) + + w2 := httptest.NewRecorder() + req2 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/top-ips?range=24h&limit=10", http.NoBody) + r.ServeHTTP(w2, req2) + assert.Equal(t, http.StatusOK, w2.Code) +} + +// --------------------------------------------------------------------------- +// DashboardScenarios edge cases +// --------------------------------------------------------------------------- + +func TestDashboardScenarios_InvalidRange(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/scenarios?range=bad", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestDashboardScenarios_Cached(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w1 := httptest.NewRecorder() + req1 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/scenarios?range=24h", http.NoBody) + r.ServeHTTP(w1, req1) + assert.Equal(t, http.StatusOK, w1.Code) + + w2 := httptest.NewRecorder() + req2 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/scenarios?range=24h", http.NoBody) + r.ServeHTTP(w2, req2) + assert.Equal(t, http.StatusOK, w2.Code) +} + +// --------------------------------------------------------------------------- +// ListAlerts edge cases +// --------------------------------------------------------------------------- + +func TestListAlerts_BadLimit(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?limit=abc", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestListAlerts_LimitCap(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?limit=999", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestListAlerts_NegativeLimit(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?limit=-1", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestListAlerts_BadOffset(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?offset=abc", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestListAlerts_NegativeOffset(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?offset=-5", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestListAlerts_Cached(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w1 := httptest.NewRecorder() + req1 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h", http.NoBody) + r.ServeHTTP(w1, req1) + assert.Equal(t, http.StatusOK, w1.Code) + + w2 := httptest.NewRecorder() + req2 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h", http.NoBody) + r.ServeHTTP(w2, req2) + assert.Equal(t, http.StatusOK, w2.Code) +} + +func TestListAlerts_ScenarioFilter(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?scenario=crowdsecurity/http-probing", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + assert.Contains(t, body, "source") +} + +// --------------------------------------------------------------------------- +// ExportDecisions edge cases +// --------------------------------------------------------------------------- + +func TestExportDecisions_InvalidRange(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/export?range=bad", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestExportDecisions_EmptyRange(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/export?format=json", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestExportDecisions_CSVWithSourceFilter(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/export?format=csv&source=crowdsec&range=7d", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Header().Get("Content-Type"), "text/csv") + body := w.Body.String() + assert.Contains(t, body, "uuid,ip,action,source,scenario") + // Verify all rows are crowdsec source + assert.NotContains(t, body, ",waf,") +} + +func TestExportDecisions_AllSources(t *testing.T) { + t.Parallel() + _, r := setupDashboardHandler(t) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/export?format=json&source=all&range=7d", http.NoBody) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + var decisions []models.SecurityDecision + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &decisions)) + // Should include both crowdsec and waf sources + assert.GreaterOrEqual(t, len(decisions), 2) +} + +// --------------------------------------------------------------------------- +// LAPI integration paths via httptest server +// --------------------------------------------------------------------------- + +func TestDashboardSummary_ActiveDecisions_LAPIReachable(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[{"id":"1"},{"id":"2"},{"id":"3"}]`)) + })) + t.Cleanup(server.Close) + + db := OpenTestDB(t) + require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{})) + + require.NoError(t, db.Create(&models.SecurityConfig{ + UUID: "default", + Name: "default", + CrowdSecAPIURL: server.URL, + }).Error) + + h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir()) + h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) } + + r := gin.New() + g := r.Group("/api/v1") + h.RegisterRoutes(g) + + now := time.Now().UTC() + require.NoError(t, db.Create(&models.SecurityDecision{ + UUID: uuid.NewString(), Source: "crowdsec", Action: "block", + IP: "10.0.0.1", Scenario: "test", CreatedAt: now.Add(-30 * time.Minute), + }).Error) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=1h", http.NoBody) + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + assert.Equal(t, float64(3), body["active_decisions"]) +} + +func TestDashboardSummary_ActiveDecisions_LAPIBadStatus(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + t.Cleanup(server.Close) + + db := OpenTestDB(t) + require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{})) + require.NoError(t, db.Create(&models.SecurityConfig{ + UUID: "default", Name: "default", CrowdSecAPIURL: server.URL, + }).Error) + + h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir()) + h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) } + + r := gin.New() + g := r.Group("/api/v1") + h.RegisterRoutes(g) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=1h", http.NoBody) + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + assert.Equal(t, float64(-1), body["active_decisions"]) +} + +func TestDashboardSummary_ActiveDecisions_LAPIBadJSON(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`not-json`)) + })) + t.Cleanup(server.Close) + + db := OpenTestDB(t) + require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{})) + require.NoError(t, db.Create(&models.SecurityConfig{ + UUID: "default", Name: "default", CrowdSecAPIURL: server.URL, + }).Error) + + h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir()) + h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) } + + r := gin.New() + g := r.Group("/api/v1") + h.RegisterRoutes(g) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=1h", http.NoBody) + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + assert.Equal(t, float64(-1), body["active_decisions"]) +} + +func TestListAlerts_LAPISuccess(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[{"id":"a1"},{"id":"a2"},{"id":"a3"},{"id":"a4"},{"id":"a5"}]`)) + })) + t.Cleanup(server.Close) + + db := OpenTestDB(t) + require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{})) + require.NoError(t, db.Create(&models.SecurityConfig{ + UUID: "default", Name: "default", CrowdSecAPIURL: server.URL, + }).Error) + + h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir()) + h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) } + + r := gin.New() + g := r.Group("/api/v1") + h.RegisterRoutes(g) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h", http.NoBody) + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + assert.Equal(t, "lapi", body["source"]) + assert.Equal(t, float64(5), body["total"]) +} + +func TestListAlerts_LAPISuccessWithOffset(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[{"id":"a1"},{"id":"a2"},{"id":"a3"},{"id":"a4"},{"id":"a5"}]`)) + })) + t.Cleanup(server.Close) + + db := OpenTestDB(t) + require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{})) + require.NoError(t, db.Create(&models.SecurityConfig{ + UUID: "default", Name: "default", CrowdSecAPIURL: server.URL, + }).Error) + + h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir()) + h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) } + + r := gin.New() + g := r.Group("/api/v1") + h.RegisterRoutes(g) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h&offset=3&limit=10", http.NoBody) + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + assert.Equal(t, "lapi", body["source"]) + assert.Equal(t, float64(5), body["total"]) + alerts := body["alerts"].([]interface{}) + assert.Equal(t, 2, len(alerts)) +} + +func TestListAlerts_LAPISuccessWithLargeOffset(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[{"id":"a1"},{"id":"a2"}]`)) + })) + t.Cleanup(server.Close) + + db := OpenTestDB(t) + require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{})) + require.NoError(t, db.Create(&models.SecurityConfig{ + UUID: "default", Name: "default", CrowdSecAPIURL: server.URL, + }).Error) + + h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir()) + h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) } + + r := gin.New() + g := r.Group("/api/v1") + h.RegisterRoutes(g) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h&offset=100", http.NoBody) + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + assert.Equal(t, "lapi", body["source"]) + // offset >= len(rawAlerts) returns nil, which marshals as JSON null + alerts := body["alerts"] + if alerts != nil { + assert.Equal(t, 0, len(alerts.([]interface{}))) + } +} + +func TestListAlerts_LAPISuccessWithLimitSlicing(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[{"id":"a1"},{"id":"a2"},{"id":"a3"},{"id":"a4"},{"id":"a5"}]`)) + })) + t.Cleanup(server.Close) + + db := OpenTestDB(t) + require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{})) + require.NoError(t, db.Create(&models.SecurityConfig{ + UUID: "default", Name: "default", CrowdSecAPIURL: server.URL, + }).Error) + + h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir()) + h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) } + + r := gin.New() + g := r.Group("/api/v1") + h.RegisterRoutes(g) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h&limit=2", http.NoBody) + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + assert.Equal(t, "lapi", body["source"]) + assert.Equal(t, float64(5), body["total"]) + alerts := body["alerts"].([]interface{}) + assert.Equal(t, 2, len(alerts)) +} + +func TestListAlerts_LAPIBadJSON(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`not-json`)) + })) + t.Cleanup(server.Close) + + db := OpenTestDB(t) + require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{})) + require.NoError(t, db.Create(&models.SecurityConfig{ + UUID: "default", Name: "default", CrowdSecAPIURL: server.URL, + }).Error) + + h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir()) + h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) } + + r := gin.New() + g := r.Group("/api/v1") + h.RegisterRoutes(g) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h", http.NoBody) + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + // Falls back to cscli + assert.Equal(t, "cscli", body["source"]) +} + +func TestListAlerts_LAPIBadStatus(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusForbidden) + })) + t.Cleanup(server.Close) + + db := OpenTestDB(t) + require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{})) + require.NoError(t, db.Create(&models.SecurityConfig{ + UUID: "default", Name: "default", CrowdSecAPIURL: server.URL, + }).Error) + + h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir()) + h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) } + + r := gin.New() + g := r.Group("/api/v1") + h.RegisterRoutes(g) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h", http.NoBody) + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + assert.Equal(t, "cscli", body["source"]) +} + +func TestListAlerts_LAPIWithScenarioFilter(t *testing.T) { + t.Parallel() + + var capturedQuery string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedQuery = r.URL.RawQuery + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[{"id":"a1"}]`)) + })) + t.Cleanup(server.Close) + + db := OpenTestDB(t) + require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{})) + require.NoError(t, db.Create(&models.SecurityConfig{ + UUID: "default", Name: "default", CrowdSecAPIURL: server.URL, + }).Error) + + h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir()) + h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) } + + r := gin.New() + g := r.Group("/api/v1") + h.RegisterRoutes(g) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h&scenario=crowdsecurity/http-probing", http.NoBody) + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, capturedQuery, "scenario=crowdsecurity") +} + +func TestFetchAlertsCscli_ErrorExec(t *testing.T) { + t.Parallel() + + db := OpenTestDB(t) + require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{})) + + h := &CrowdsecHandler{ + DB: db, + Executor: &fakeExec{}, + CmdExec: &mockCmdExecutor{output: nil, err: fmt.Errorf("cscli not found")}, + BinPath: "/bin/false", + DataDir: t.TempDir(), + dashCache: newDashboardCache(), + } + + r := gin.New() + g := r.Group("/api/v1") + h.RegisterRoutes(g) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h", http.NoBody) + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + assert.Equal(t, "cscli", body["source"]) + assert.Equal(t, float64(0), body["total"]) +} + +func TestFetchAlertsCscli_ValidJSON(t *testing.T) { + t.Parallel() + + db := OpenTestDB(t) + require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{})) + + h := &CrowdsecHandler{ + DB: db, + Executor: &fakeExec{}, + CmdExec: &mockCmdExecutor{output: []byte(`[{"id":"1"},{"id":"2"}]`), err: nil}, + BinPath: "/bin/false", + DataDir: t.TempDir(), + dashCache: newDashboardCache(), + } + + r := gin.New() + g := r.Group("/api/v1") + h.RegisterRoutes(g) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h", http.NoBody) + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + var body map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + assert.Equal(t, "cscli", body["source"]) + assert.Equal(t, float64(2), body["total"]) +} diff --git a/backend/internal/api/handlers/crowdsec_decisions_test.go b/backend/internal/api/handlers/crowdsec_decisions_test.go index 1ef9c26a1..00d4097ef 100644 --- a/backend/internal/api/handlers/crowdsec_decisions_test.go +++ b/backend/internal/api/handlers/crowdsec_decisions_test.go @@ -28,7 +28,6 @@ func (m *mockCommandExecutor) Execute(ctx context.Context, name string, args ... } func TestListDecisions_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -67,7 +66,6 @@ func TestListDecisions_Success(t *testing.T) { } func TestListDecisions_EmptyList(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -98,7 +96,6 @@ func TestListDecisions_EmptyList(t *testing.T) { } func TestListDecisions_CscliError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -130,7 +127,6 @@ func TestListDecisions_CscliError(t *testing.T) { } func TestListDecisions_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -154,7 +150,6 @@ func TestListDecisions_InvalidJSON(t *testing.T) { } func TestBanIP_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -205,7 +200,6 @@ func TestBanIP_Success(t *testing.T) { } func TestBanIP_DefaultDuration(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -245,7 +239,6 @@ func TestBanIP_DefaultDuration(t *testing.T) { } func TestBanIP_MissingIP(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -268,7 +261,6 @@ func TestBanIP_MissingIP(t *testing.T) { } func TestBanIP_EmptyIP(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -293,7 +285,6 @@ func TestBanIP_EmptyIP(t *testing.T) { } func TestBanIP_CscliError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -323,7 +314,6 @@ func TestBanIP_CscliError(t *testing.T) { } func TestUnbanIP_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -357,7 +347,6 @@ func TestUnbanIP_Success(t *testing.T) { } func TestUnbanIP_CscliError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -381,7 +370,6 @@ func TestUnbanIP_CscliError(t *testing.T) { } func TestListDecisions_MultipleDecisions(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -430,7 +418,6 @@ func TestListDecisions_MultipleDecisions(t *testing.T) { } func TestBanIP_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() diff --git a/backend/internal/api/handlers/crowdsec_handler.go b/backend/internal/api/handlers/crowdsec_handler.go index 1b8f9a5d8..b6bbf66c2 100644 --- a/backend/internal/api/handlers/crowdsec_handler.go +++ b/backend/internal/api/handlers/crowdsec_handler.go @@ -66,6 +66,12 @@ type CrowdsecHandler struct { CaddyManager *caddy.Manager // For config reload after bouncer registration LAPIMaxWait time.Duration // For testing; 0 means 60s default LAPIPollInterval time.Duration // For testing; 0 means 500ms default + dashCache *dashboardCache + + // validateLAPIURL validates and parses a LAPI base URL. + // This field allows tests to inject a permissive validator for mock servers + // without mutating package-level state (which causes data races). + validateLAPIURL func(string) (*url.URL, error) // registrationMutex protects concurrent bouncer registration attempts registrationMutex sync.Mutex @@ -84,6 +90,14 @@ const ( bouncerName = "caddy-bouncer" ) +// resolveLAPIURLValidator returns the handler's validator or the default. +func (h *CrowdsecHandler) resolveLAPIURLValidator(raw string) (*url.URL, error) { + if h.validateLAPIURL != nil { + return h.validateLAPIURL(raw) + } + return validateCrowdsecLAPIBaseURLDefault(raw) +} + func (h *CrowdsecHandler) bouncerKeyPath() string { if h != nil && strings.TrimSpace(h.DataDir) != "" { return filepath.Join(h.DataDir, "bouncer_key") @@ -370,14 +384,16 @@ func NewCrowdsecHandler(db *gorm.DB, executor CrowdsecExecutor, binPath, dataDir consoleSvc = crowdsec.NewConsoleEnrollmentService(db, &crowdsec.SecureCommandExecutor{}, dataDir, consoleSecret) } return &CrowdsecHandler{ - DB: db, - Executor: executor, - CmdExec: &RealCommandExecutor{}, - BinPath: binPath, - DataDir: dataDir, - Hub: hubSvc, - Console: consoleSvc, - Security: securitySvc, + DB: db, + Executor: executor, + CmdExec: &RealCommandExecutor{}, + BinPath: binPath, + DataDir: dataDir, + Hub: hubSvc, + Console: consoleSvc, + Security: securitySvc, + dashCache: newDashboardCache(), + validateLAPIURL: validateCrowdsecLAPIBaseURLDefault, } } @@ -1442,18 +1458,10 @@ const ( defaultCrowdsecLAPIPort = 8085 ) -// validateCrowdsecLAPIBaseURLFunc is a variable holding the LAPI URL validation function. -// This indirection allows tests to inject a permissive validator for mock servers. -var validateCrowdsecLAPIBaseURLFunc = validateCrowdsecLAPIBaseURLDefault - func validateCrowdsecLAPIBaseURLDefault(raw string) (*url.URL, error) { return security.ValidateInternalServiceBaseURL(raw, defaultCrowdsecLAPIPort, security.InternalServiceHostAllowlist()) } -func validateCrowdsecLAPIBaseURL(raw string) (*url.URL, error) { - return validateCrowdsecLAPIBaseURLFunc(raw) -} - // GetLAPIDecisions queries CrowdSec LAPI directly for current decisions. // This is an alternative to ListDecisions which uses cscli. // Query params: @@ -1471,7 +1479,7 @@ func (h *CrowdsecHandler) GetLAPIDecisions(c *gin.Context) { } } - baseURL, err := validateCrowdsecLAPIBaseURL(lapiURL) + baseURL, err := h.resolveLAPIURLValidator(lapiURL) if err != nil { logger.Log().WithError(err).WithField("lapi_url", lapiURL).Warn("Blocked CrowdSec LAPI URL by internal allowlist policy") // Fallback to cscli-based method. @@ -2142,7 +2150,7 @@ func (h *CrowdsecHandler) CheckLAPIHealth(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) defer cancel() - baseURL, err := validateCrowdsecLAPIBaseURL(lapiURL) + baseURL, err := h.resolveLAPIURLValidator(lapiURL) if err != nil { c.JSON(http.StatusOK, gin.H{"healthy": false, "error": "invalid LAPI URL (blocked by SSRF policy)", "lapi_url": lapiURL}) return @@ -2287,6 +2295,21 @@ func (h *CrowdsecHandler) BanIP(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{"status": "banned", "ip": ip, "duration": duration}) + + // Log to security_decisions for dashboard aggregation + if h.Security != nil { + parsedDur, _ := time.ParseDuration(duration) + expiry := time.Now().Add(parsedDur) + _ = h.Security.LogDecision(&models.SecurityDecision{ + IP: ip, + Action: "block", + Source: "crowdsec", + RuleID: reason, + Scenario: "manual", + ExpiresAt: &expiry, + }) + } + h.dashCache.Invalidate("dashboard") } // UnbanIP removes a ban for an IP address @@ -2313,6 +2336,7 @@ func (h *CrowdsecHandler) UnbanIP(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{"status": "unbanned", "ip": ip}) + h.dashCache.Invalidate("dashboard") } // RegisterBouncer registers a new bouncer or returns existing bouncer status. @@ -2711,4 +2735,11 @@ func (h *CrowdsecHandler) RegisterRoutes(rg *gin.RouterGroup) { // Acquisition configuration endpoints rg.GET("/admin/crowdsec/acquisition", h.GetAcquisitionConfig) rg.PUT("/admin/crowdsec/acquisition", h.UpdateAcquisitionConfig) + // Dashboard aggregation endpoints (PR-1) + rg.GET("/admin/crowdsec/dashboard/summary", h.DashboardSummary) + rg.GET("/admin/crowdsec/dashboard/timeline", h.DashboardTimeline) + rg.GET("/admin/crowdsec/dashboard/top-ips", h.DashboardTopIPs) + rg.GET("/admin/crowdsec/dashboard/scenarios", h.DashboardScenarios) + rg.GET("/admin/crowdsec/alerts", h.ListAlerts) + rg.GET("/admin/crowdsec/decisions/export", h.ExportDecisions) } diff --git a/backend/internal/api/handlers/crowdsec_handler_comprehensive_test.go b/backend/internal/api/handlers/crowdsec_handler_comprehensive_test.go index 3b9a9e4a6..38ca0826d 100644 --- a/backend/internal/api/handlers/crowdsec_handler_comprehensive_test.go +++ b/backend/internal/api/handlers/crowdsec_handler_comprehensive_test.go @@ -106,7 +106,6 @@ func TestMapCrowdsecStatus(t *testing.T) { // TestIsConsoleEnrollmentEnabled tests the isConsoleEnrollmentEnabled helper func TestIsConsoleEnrollmentEnabled(t *testing.T) { - gin.SetMode(gin.TestMode) tests := []struct { name string @@ -191,7 +190,6 @@ func TestActorFromContext(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) tt.setupCtx(c) @@ -204,7 +202,6 @@ func TestActorFromContext(t *testing.T) { // TestHubEndpoints tests the hubEndpoints helper func TestHubEndpoints(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) tmpDir := t.TempDir() @@ -233,7 +230,6 @@ func TestHubEndpoints(t *testing.T) { // TestGetCachedPreset tests the GetCachedPreset handler func TestGetCachedPreset(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) tmpDir := t.TempDir() @@ -264,7 +260,6 @@ func TestGetCachedPreset(t *testing.T) { // TestGetCachedPreset_NotFound tests GetCachedPreset with non-existent preset func TestGetCachedPreset_NotFound(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) tmpDir := t.TempDir() @@ -293,7 +288,6 @@ func TestGetCachedPreset_NotFound(t *testing.T) { // TestGetLAPIDecisions tests the GetLAPIDecisions handler func TestGetLAPIDecisions(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) tmpDir := t.TempDir() @@ -313,7 +307,6 @@ func TestGetLAPIDecisions(t *testing.T) { // TestCheckLAPIHealth tests the CheckLAPIHealth handler func TestCheckLAPIHealth(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) tmpDir := t.TempDir() @@ -332,7 +325,6 @@ func TestCheckLAPIHealth(t *testing.T) { // TestListDecisions tests the ListDecisions handler func TestListDecisions(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) tmpDir := t.TempDir() @@ -351,7 +343,6 @@ func TestListDecisions(t *testing.T) { // TestBanIP tests the BanIP handler func TestBanIP(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) tmpDir := t.TempDir() @@ -373,7 +364,6 @@ func TestBanIP(t *testing.T) { // TestUnbanIP tests the UnbanIP handler func TestUnbanIP(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) tmpDir := t.TempDir() @@ -395,7 +385,6 @@ func TestUnbanIP(t *testing.T) { // TestGetAcquisitionConfig tests the GetAcquisitionConfig handler func TestGetAcquisitionConfig(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) tmpDir := t.TempDir() acquisPath := filepath.Join(tmpDir, "acquis.yaml") @@ -417,7 +406,6 @@ func TestGetAcquisitionConfig(t *testing.T) { // TestUpdateAcquisitionConfig tests the UpdateAcquisitionConfig handler func TestUpdateAcquisitionConfig(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) tmpDir := t.TempDir() acquisPath := filepath.Join(tmpDir, "acquis.yaml") diff --git a/backend/internal/api/handlers/crowdsec_handler_coverage_test.go b/backend/internal/api/handlers/crowdsec_handler_coverage_test.go index 1a82ad981..5e6c7c8db 100644 --- a/backend/internal/api/handlers/crowdsec_handler_coverage_test.go +++ b/backend/internal/api/handlers/crowdsec_handler_coverage_test.go @@ -29,7 +29,6 @@ func (f *errorExec) Status(ctx context.Context, configDir string) (running bool, } func TestCrowdsec_Start_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -48,7 +47,6 @@ func TestCrowdsec_Start_Error(t *testing.T) { } func TestCrowdsec_Stop_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -67,7 +65,6 @@ func TestCrowdsec_Stop_Error(t *testing.T) { } func TestCrowdsec_Status_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -87,7 +84,6 @@ func TestCrowdsec_Status_Error(t *testing.T) { // ReadFile tests func TestCrowdsec_ReadFile_MissingPath(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -106,7 +102,6 @@ func TestCrowdsec_ReadFile_MissingPath(t *testing.T) { } func TestCrowdsec_ReadFile_PathTraversal(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -126,7 +121,6 @@ func TestCrowdsec_ReadFile_PathTraversal(t *testing.T) { } func TestCrowdsec_ReadFile_NotFound(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -146,7 +140,6 @@ func TestCrowdsec_ReadFile_NotFound(t *testing.T) { // WriteFile tests func TestCrowdsec_WriteFile_InvalidPayload(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -166,7 +159,6 @@ func TestCrowdsec_WriteFile_InvalidPayload(t *testing.T) { } func TestCrowdsec_WriteFile_MissingPath(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -189,7 +181,6 @@ func TestCrowdsec_WriteFile_MissingPath(t *testing.T) { } func TestCrowdsec_WriteFile_PathTraversal(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -214,7 +205,6 @@ func TestCrowdsec_WriteFile_PathTraversal(t *testing.T) { // ExportConfig tests func TestCrowdsec_ExportConfig_NotFound(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) // Use a non-existent directory nonExistentDir := "/tmp/crowdsec-nonexistent-dir-12345" @@ -238,7 +228,6 @@ func TestCrowdsec_ExportConfig_NotFound(t *testing.T) { // ListFiles tests func TestCrowdsec_ListFiles_EmptyDir(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -263,7 +252,6 @@ func TestCrowdsec_ListFiles_EmptyDir(t *testing.T) { } func TestCrowdsec_ListFiles_NonExistent(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) nonExistentDir := "/tmp/crowdsec-nonexistent-dir-67890" _ = os.RemoveAll(nonExistentDir) @@ -289,7 +277,6 @@ func TestCrowdsec_ListFiles_NonExistent(t *testing.T) { // ImportConfig error cases func TestCrowdsec_ImportConfig_NoFile(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -310,7 +297,6 @@ func TestCrowdsec_ImportConfig_NoFile(t *testing.T) { // Additional ReadFile test with nested path that exists func TestCrowdsec_ReadFile_NestedPath(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -336,7 +322,6 @@ func TestCrowdsec_ReadFile_NestedPath(t *testing.T) { // Test WriteFile when backup fails (simulate by making dir unwritable) func TestCrowdsec_WriteFile_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -364,7 +349,6 @@ func TestCrowdsec_WriteFile_Success(t *testing.T) { } func TestCrowdsec_ListPresets_Disabled(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) t.Setenv("FEATURE_CERBERUS_ENABLED", "false") tmpDir := t.TempDir() @@ -383,7 +367,6 @@ func TestCrowdsec_ListPresets_Disabled(t *testing.T) { } func TestCrowdsec_ListPresets_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -406,7 +389,6 @@ func TestCrowdsec_ListPresets_Success(t *testing.T) { } func TestCrowdsec_PullPreset_Validation(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -431,7 +413,6 @@ func TestCrowdsec_PullPreset_Validation(t *testing.T) { } func TestCrowdsec_ApplyPreset_Validation(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() diff --git a/backend/internal/api/handlers/crowdsec_handler_test.go b/backend/internal/api/handlers/crowdsec_handler_test.go index bf72edb18..659e17c3a 100644 --- a/backend/internal/api/handlers/crowdsec_handler_test.go +++ b/backend/internal/api/handlers/crowdsec_handler_test.go @@ -89,7 +89,6 @@ func newTestCrowdsecHandler(t *testing.T, db *gorm.DB, executor CrowdsecExecutor func TestCrowdsecEndpoints(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -127,7 +126,6 @@ func TestCrowdsecEndpoints(t *testing.T) { func TestImportConfig(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() fe := &fakeExec{} @@ -173,7 +171,6 @@ func TestImportConfig(t *testing.T) { func TestImportCreatesBackup(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() // create existing config dir with a marker file @@ -240,7 +237,6 @@ func TestImportCreatesBackup(t *testing.T) { func TestExportConfig(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -272,7 +268,6 @@ func TestExportConfig(t *testing.T) { func TestListAndReadFile(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() // create a nested file @@ -304,7 +299,6 @@ func TestListAndReadFile(t *testing.T) { func TestExportConfigStreamsArchive(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) dataDir := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(dataDir, "config.yaml"), []byte("hello"), 0o600)) // #nosec G306 -- test fixture @@ -345,7 +339,6 @@ func TestExportConfigStreamsArchive(t *testing.T) { func TestWriteFileCreatesBackup(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() // create existing config dir with a marker file @@ -384,7 +377,6 @@ func TestWriteFileCreatesBackup(t *testing.T) { } func TestListPresetsCerberusDisabled(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "false") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -403,7 +395,6 @@ func TestListPresetsCerberusDisabled(t *testing.T) { func TestReadFileInvalidPath(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() g := r.Group("/api/v1") @@ -420,7 +411,6 @@ func TestReadFileInvalidPath(t *testing.T) { func TestWriteFileInvalidPath(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() g := r.Group("/api/v1") @@ -439,7 +429,6 @@ func TestWriteFileInvalidPath(t *testing.T) { func TestWriteFileMissingPath(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() g := r.Group("/api/v1") @@ -456,7 +445,6 @@ func TestWriteFileMissingPath(t *testing.T) { func TestWriteFileInvalidPayload(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() g := r.Group("/api/v1") @@ -472,7 +460,6 @@ func TestWriteFileInvalidPayload(t *testing.T) { func TestImportConfigRequiresFile(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() g := r.Group("/api/v1") @@ -489,7 +476,6 @@ func TestImportConfigRequiresFile(t *testing.T) { func TestImportConfigRejectsEmptyUpload(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() g := r.Group("/api/v1") @@ -513,7 +499,6 @@ func TestImportConfigRejectsEmptyUpload(t *testing.T) { func TestListFilesMissingDir(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) missingDir := filepath.Join(t.TempDir(), "does-not-exist") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", missingDir) @@ -532,7 +517,6 @@ func TestListFilesMissingDir(t *testing.T) { func TestListFilesReturnsEntries(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) dataDir := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(dataDir, "root.txt"), []byte("root"), 0o600)) // #nosec G306 -- test fixture nestedDir := filepath.Join(dataDir, "nested") @@ -562,7 +546,6 @@ func TestListFilesReturnsEntries(t *testing.T) { func TestIsCerberusEnabledFromDB(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{})) require.NoError(t, db.Create(&models.Setting{Key: "feature.cerberus.enabled", Value: "0"}).Error) @@ -582,7 +565,6 @@ func TestIsCerberusEnabledFromDB(t *testing.T) { } func TestIsCerberusEnabledInvalidEnv(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "not-a-bool") h := newTestCrowdsecHandler(t, nil, &fakeExec{}, "/bin/false", t.TempDir()) @@ -592,7 +574,6 @@ func TestIsCerberusEnabledInvalidEnv(t *testing.T) { } func TestIsCerberusEnabledLegacyEnv(t *testing.T) { - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, nil, &fakeExec{}, "/bin/false", t.TempDir()) t.Setenv("CERBERUS_ENABLED", "0") @@ -636,7 +617,6 @@ func (m *mockEnvExecutor) ExecuteWithEnv(ctx context.Context, name string, args func setupTestConsoleEnrollment(t *testing.T) (*CrowdsecHandler, *mockEnvExecutor) { t.Helper() - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.CrowdsecConsoleEnrollment{})) @@ -651,7 +631,6 @@ func setupTestConsoleEnrollment(t *testing.T) (*CrowdsecHandler, *mockEnvExecuto } func TestConsoleEnrollDisabled(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "false") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -670,7 +649,6 @@ func TestConsoleEnrollDisabled(t *testing.T) { } func TestConsoleEnrollServiceUnavailable(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "true") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -691,7 +669,6 @@ func TestConsoleEnrollServiceUnavailable(t *testing.T) { } func TestConsoleEnrollInvalidPayload(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "true") h, _ := setupTestConsoleEnrollment(t) @@ -709,7 +686,6 @@ func TestConsoleEnrollInvalidPayload(t *testing.T) { } func TestConsoleEnrollSuccess(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "true") h, _ := setupTestConsoleEnrollment(t) @@ -732,7 +708,6 @@ func TestConsoleEnrollSuccess(t *testing.T) { } func TestConsoleEnrollMissingAgentName(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "true") h, _ := setupTestConsoleEnrollment(t) @@ -751,7 +726,6 @@ func TestConsoleEnrollMissingAgentName(t *testing.T) { } func TestConsoleStatusDisabled(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "false") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -768,7 +742,6 @@ func TestConsoleStatusDisabled(t *testing.T) { } func TestConsoleStatusServiceUnavailable(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "true") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -787,7 +760,6 @@ func TestConsoleStatusServiceUnavailable(t *testing.T) { } func TestConsoleStatusSuccess(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "true") h, _ := setupTestConsoleEnrollment(t) @@ -808,7 +780,6 @@ func TestConsoleStatusSuccess(t *testing.T) { } func TestConsoleStatusAfterEnroll(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "true") h, _ := setupTestConsoleEnrollment(t) @@ -844,7 +815,6 @@ func TestConsoleStatusAfterEnroll(t *testing.T) { func TestIsConsoleEnrollmentEnabledFromDB(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{})) require.NoError(t, db.Create(&models.Setting{Key: "feature.crowdsec.console_enrollment", Value: "true"}).Error) @@ -855,7 +825,6 @@ func TestIsConsoleEnrollmentEnabledFromDB(t *testing.T) { func TestIsConsoleEnrollmentDisabledFromDB(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{})) require.NoError(t, db.Create(&models.Setting{Key: "feature.crowdsec.console_enrollment", Value: "false"}).Error) @@ -865,7 +834,6 @@ func TestIsConsoleEnrollmentDisabledFromDB(t *testing.T) { } func TestIsConsoleEnrollmentEnabledFromEnv(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "true") h := newTestCrowdsecHandler(t, nil, &fakeExec{}, "/bin/false", t.TempDir()) @@ -873,7 +841,6 @@ func TestIsConsoleEnrollmentEnabledFromEnv(t *testing.T) { } func TestIsConsoleEnrollmentDisabledFromEnv(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "0") h := newTestCrowdsecHandler(t, nil, &fakeExec{}, "/bin/false", t.TempDir()) @@ -881,7 +848,6 @@ func TestIsConsoleEnrollmentDisabledFromEnv(t *testing.T) { } func TestIsConsoleEnrollmentInvalidEnv(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "invalid") h := newTestCrowdsecHandler(t, nil, &fakeExec{}, "/bin/false", t.TempDir()) @@ -889,7 +855,6 @@ func TestIsConsoleEnrollmentInvalidEnv(t *testing.T) { } func TestIsConsoleEnrollmentDefaultDisabled(t *testing.T) { - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, nil, &fakeExec{}, "/bin/false", t.TempDir()) require.False(t, h.isConsoleEnrollmentEnabled()) @@ -914,7 +879,6 @@ func TestIsConsoleEnrollmentDBTrueVariants(t *testing.T) { for _, tc := range tests { t.Run(tc.value, func(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{})) require.NoError(t, db.Create(&models.Setting{Key: "feature.crowdsec.console_enrollment", Value: tc.value}).Error) @@ -948,7 +912,6 @@ func (m *mockCmdExecutor) Execute(ctx context.Context, name string, args ...stri func TestRegisterBouncerScriptNotFound(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() g := r.Group("/api/v1") @@ -965,7 +928,6 @@ func TestRegisterBouncerScriptNotFound(t *testing.T) { func TestRegisterBouncerSuccess(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) // Create a temp script that mimics successful bouncer registration tmpDir := t.TempDir() @@ -1003,7 +965,6 @@ func TestRegisterBouncerSuccess(t *testing.T) { func TestRegisterBouncerExecutionError(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) // Create a mock command executor that simulates execution error mockExec := &mockCmdExecutor{ @@ -1032,7 +993,6 @@ func TestRegisterBouncerExecutionError(t *testing.T) { // ============================================ func TestGetAcquisitionConfigNotFound(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("CHARON_CROWDSEC_ACQUIS_PATH", filepath.Join(t.TempDir(), "missing-acquis.yaml")) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() @@ -1048,7 +1008,6 @@ func TestGetAcquisitionConfigNotFound(t *testing.T) { } func TestGetAcquisitionConfigSuccess(t *testing.T) { - gin.SetMode(gin.TestMode) // Create a temp acquis.yaml to test with tmpDir := t.TempDir() @@ -1087,7 +1046,6 @@ labels: // ============================================ func TestDeleteConsoleEnrollmentDisabled(t *testing.T) { - gin.SetMode(gin.TestMode) // Feature flag not set, should return 404 h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -1104,7 +1062,6 @@ func TestDeleteConsoleEnrollmentDisabled(t *testing.T) { } func TestDeleteConsoleEnrollmentServiceUnavailable(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "true") // Create handler with nil Console service @@ -1131,7 +1088,6 @@ func TestDeleteConsoleEnrollmentServiceUnavailable(t *testing.T) { } func TestDeleteConsoleEnrollmentSuccess(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "true") h, _ := setupTestConsoleEnrollment(t) @@ -1164,7 +1120,6 @@ func TestDeleteConsoleEnrollmentSuccess(t *testing.T) { } func TestDeleteConsoleEnrollmentNoRecordSuccess(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "true") h, _ := setupTestConsoleEnrollment(t) @@ -1184,7 +1139,6 @@ func TestDeleteConsoleEnrollmentNoRecordSuccess(t *testing.T) { } func TestDeleteConsoleEnrollmentThenReenroll(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "true") h, _ := setupTestConsoleEnrollment(t) @@ -1252,7 +1206,6 @@ func TestDeleteConsoleEnrollmentThenReenroll(t *testing.T) { // Start Handler - LAPI Readiness Polling Tests func TestCrowdsecStart_LAPINotReadyTimeout(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) // Mock executor that returns error for lapi status checks mockExec := &mockCmdExecutor{ @@ -1311,7 +1264,6 @@ func (f *fakeExecWithError) Status(ctx context.Context, configDir string) (runni func TestCrowdsecHandler_Status_Error(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) fe := &fakeExecWithError{statusError: errors.New("status check failed")} db := setupCrowdDB(t) @@ -1331,7 +1283,6 @@ func TestCrowdsecHandler_Status_Error(t *testing.T) { func TestCrowdsecHandler_Start_ExecutorError(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) fe := &fakeExecWithError{startError: errors.New("failed to start process")} db := setupCrowdDB(t) @@ -1351,7 +1302,6 @@ func TestCrowdsecHandler_Start_ExecutorError(t *testing.T) { func TestCrowdsecHandler_ExportConfig_DirNotFound(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) // Use a non-existent directory @@ -1376,7 +1326,6 @@ func TestCrowdsecHandler_ExportConfig_DirNotFound(t *testing.T) { func TestCrowdsecHandler_ReadFile_NotFound(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -1396,7 +1345,6 @@ func TestCrowdsecHandler_ReadFile_NotFound(t *testing.T) { func TestCrowdsecHandler_ReadFile_MissingPath(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir()) @@ -1415,7 +1363,6 @@ func TestCrowdsecHandler_ReadFile_MissingPath(t *testing.T) { func TestCrowdsecHandler_ListDecisions_Success(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) // Mock executor that returns valid JSON decisions mockExec := &mockCmdExecutor{ @@ -1444,7 +1391,6 @@ func TestCrowdsecHandler_ListDecisions_Success(t *testing.T) { func TestCrowdsecHandler_ListDecisions_Empty(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) // Mock executor that returns null (no decisions) mockExec := &mockCmdExecutor{ @@ -1472,7 +1418,6 @@ func TestCrowdsecHandler_ListDecisions_Empty(t *testing.T) { func TestCrowdsecHandler_ListDecisions_CscliError(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) // Mock executor that returns an error mockExec := &mockCmdExecutor{ @@ -1498,7 +1443,6 @@ func TestCrowdsecHandler_ListDecisions_CscliError(t *testing.T) { func TestCrowdsecHandler_ListDecisions_InvalidJSON(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) // Mock executor that returns invalid JSON mockExec := &mockCmdExecutor{ @@ -1524,7 +1468,6 @@ func TestCrowdsecHandler_ListDecisions_InvalidJSON(t *testing.T) { func TestCrowdsecHandler_BanIP_Success(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) mockExec := &mockCmdExecutor{ output: []byte("Decision created"), @@ -1554,7 +1497,6 @@ func TestCrowdsecHandler_BanIP_Success(t *testing.T) { func TestCrowdsecHandler_BanIP_MissingIP(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir()) @@ -1575,7 +1517,6 @@ func TestCrowdsecHandler_BanIP_MissingIP(t *testing.T) { func TestCrowdsecHandler_BanIP_EmptyIP(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir()) @@ -1596,7 +1537,6 @@ func TestCrowdsecHandler_BanIP_EmptyIP(t *testing.T) { func TestCrowdsecHandler_BanIP_DefaultDuration(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) mockExec := &mockCmdExecutor{ output: []byte("Decision created"), @@ -1626,7 +1566,6 @@ func TestCrowdsecHandler_BanIP_DefaultDuration(t *testing.T) { func TestCrowdsecHandler_UnbanIP_Success(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) mockExec := &mockCmdExecutor{ output: []byte("Decision deleted"), @@ -1653,7 +1592,6 @@ func TestCrowdsecHandler_UnbanIP_Success(t *testing.T) { func TestCrowdsecHandler_UnbanIP_Error(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) mockExec := &mockCmdExecutor{ output: []byte("error"), @@ -1682,7 +1620,6 @@ func TestCrowdsecHandler_UnbanIP_Error(t *testing.T) { func TestCrowdsecHandler_BanIP_ExecutionError(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) mockExec := &mockCmdExecutor{ output: []byte("error: failed to add decision"), @@ -1711,7 +1648,6 @@ func TestCrowdsecHandler_BanIP_ExecutionError(t *testing.T) { func TestCrowdsecHandler_CheckLAPIHealth_InvalidURL(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -1747,7 +1683,6 @@ func TestCrowdsecHandler_CheckLAPIHealth_InvalidURL(t *testing.T) { func TestCrowdsecHandler_GetLAPIDecisions_Fallback(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) // Mock executor that simulates fallback to cscli mockExec := &mockCmdExecutor{ @@ -1786,7 +1721,6 @@ func TestCrowdsecHandler_GetLAPIDecisions_Fallback(t *testing.T) { } func TestCrowdsecHandler_PullPreset_CerberusDisabled(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "false") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -1805,7 +1739,6 @@ func TestCrowdsecHandler_PullPreset_CerberusDisabled(t *testing.T) { } func TestCrowdsecHandler_PullPreset_InvalidPayload(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -1823,7 +1756,6 @@ func TestCrowdsecHandler_PullPreset_InvalidPayload(t *testing.T) { } func TestCrowdsecHandler_PullPreset_EmptySlug(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -1842,7 +1774,6 @@ func TestCrowdsecHandler_PullPreset_EmptySlug(t *testing.T) { } func TestCrowdsecHandler_PullPreset_HubUnavailable(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -1863,7 +1794,6 @@ func TestCrowdsecHandler_PullPreset_HubUnavailable(t *testing.T) { } func TestCrowdsecHandler_ApplyPreset_CerberusDisabled(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "false") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -1882,7 +1812,6 @@ func TestCrowdsecHandler_ApplyPreset_CerberusDisabled(t *testing.T) { } func TestCrowdsecHandler_ApplyPreset_InvalidPayload(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -1900,7 +1829,6 @@ func TestCrowdsecHandler_ApplyPreset_InvalidPayload(t *testing.T) { } func TestCrowdsecHandler_ApplyPreset_EmptySlug(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -1919,7 +1847,6 @@ func TestCrowdsecHandler_ApplyPreset_EmptySlug(t *testing.T) { } func TestCrowdsecHandler_ApplyPreset_HubUnavailable(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -1941,7 +1868,6 @@ func TestCrowdsecHandler_ApplyPreset_HubUnavailable(t *testing.T) { func TestCrowdsecHandler_UpdateAcquisitionConfig_MissingContent(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() @@ -1960,7 +1886,6 @@ func TestCrowdsecHandler_UpdateAcquisitionConfig_MissingContent(t *testing.T) { func TestCrowdsecHandler_UpdateAcquisitionConfig_InvalidJSON(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() @@ -1977,7 +1902,6 @@ func TestCrowdsecHandler_UpdateAcquisitionConfig_InvalidJSON(t *testing.T) { func TestCrowdsecHandler_ListDecisions_WithConfigYaml(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() // Create config.yaml to trigger the config path code @@ -2018,7 +1942,6 @@ func TestCrowdsecHandler_ListDecisions_WithConfigYaml(t *testing.T) { func TestCrowdsecHandler_BanIP_WithConfigYaml(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() // Create config.yaml to trigger the config path code @@ -2048,7 +1971,6 @@ func TestCrowdsecHandler_BanIP_WithConfigYaml(t *testing.T) { func TestCrowdsecHandler_UnbanIP_WithConfigYaml(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() // Create config.yaml to trigger the config path code @@ -2076,7 +1998,6 @@ func TestCrowdsecHandler_UnbanIP_WithConfigYaml(t *testing.T) { func TestCrowdsecHandler_Status_LAPIReady(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() // Create config.yaml @@ -2112,7 +2033,6 @@ func TestCrowdsecHandler_Status_LAPIReady(t *testing.T) { func TestCrowdsecHandler_Status_LAPINotReady(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() @@ -2146,7 +2066,6 @@ func TestCrowdsecHandler_Status_LAPINotReady(t *testing.T) { func TestCrowdsecHandler_ListDecisions_WithCreatedAt(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) // Mock executor that returns decisions with created_at field mockExec := &mockCmdExecutor{ @@ -2180,7 +2099,6 @@ func TestCrowdsecHandler_ListDecisions_WithCreatedAt(t *testing.T) { func TestCrowdsecHandler_HubEndpoints(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) // Test with nil Hub h := &CrowdsecHandler{Hub: nil} @@ -2196,7 +2114,6 @@ func TestCrowdsecHandler_HubEndpoints(t *testing.T) { } func TestCrowdsecHandler_ConsoleEnroll_ProgressConflict(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "true") h, _ := setupTestConsoleEnrollment(t) @@ -2224,7 +2141,6 @@ func TestCrowdsecHandler_ConsoleEnroll_ProgressConflict(t *testing.T) { } func TestCrowdsecHandler_GetCachedPreset_CerberusDisabled(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "false") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -2241,7 +2157,6 @@ func TestCrowdsecHandler_GetCachedPreset_CerberusDisabled(t *testing.T) { } func TestCrowdsecHandler_GetCachedPreset_HubUnavailable(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -2261,7 +2176,6 @@ func TestCrowdsecHandler_GetCachedPreset_HubUnavailable(t *testing.T) { } func TestCrowdsecHandler_GetCachedPreset_EmptySlug(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") db := OpenTestDB(t) @@ -2283,7 +2197,6 @@ func TestCrowdsecHandler_GetCachedPreset_EmptySlug(t *testing.T) { // TestCrowdsecHandler_Start_StatusCode tests starting CrowdSec returns 200 status func TestCrowdsecHandler_Start_StatusCode(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() fe := &fakeExec{} @@ -2307,7 +2220,6 @@ func TestCrowdsecHandler_Start_StatusCode(t *testing.T) { // TestCrowdsecHandler_Stop_UpdatesSecurityConfig tests stopping CrowdSec updates SecurityConfig func TestCrowdsecHandler_Stop_UpdatesSecurityConfig(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() fe := &fakeExec{started: true} @@ -2342,7 +2254,6 @@ func TestCrowdsecHandler_Stop_UpdatesSecurityConfig(t *testing.T) { // TestCrowdsecHandler_ActorFromContext tests actor extraction from Gin context func TestCrowdsecHandler_ActorFromContext(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) // Test with userID present c1, _ := gin.CreateTestContext(httptest.NewRecorder()) @@ -2359,7 +2270,6 @@ func TestCrowdsecHandler_ActorFromContext(t *testing.T) { // TestCrowdsecHandler_IsCerberusEnabled_EnvVar tests Cerberus feature flag via environment variable func TestCrowdsecHandler_IsCerberusEnabled_EnvVar(t *testing.T) { // Note: Cannot use t.Parallel() with t.Setenv in subtests - gin.SetMode(gin.TestMode) testCases := []struct { name string @@ -2397,7 +2307,6 @@ func TestCrowdsecHandler_IsCerberusEnabled_EnvVar(t *testing.T) { // TestCrowdsecHandler_ApplyPreset_InvalidJSON verifies JSON binding error handling func TestCrowdsecHandler_ApplyPreset_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -2416,7 +2325,6 @@ func TestCrowdsecHandler_ApplyPreset_InvalidJSON(t *testing.T) { // TestCrowdsecHandler_ApplyPreset_MissingPresetFile verifies cache miss handling func TestCrowdsecHandler_ApplyPreset_MissingPresetFile(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") db := OpenTestDB(t) @@ -2444,7 +2352,6 @@ func TestCrowdsecHandler_ApplyPreset_MissingPresetFile(t *testing.T) { // TestCrowdsecHandler_GetPresets_DirectoryReadError simulates directory access errors func TestCrowdsecHandler_GetPresets_DirectoryReadError(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") db := OpenTestDB(t) @@ -2480,7 +2387,6 @@ func TestCrowdsecHandler_GetPresets_DirectoryReadError(t *testing.T) { // TestCrowdsecHandler_Start_AlreadyRunning verifies Start when process is already running func TestCrowdsecHandler_Start_AlreadyRunning(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) // Create executor that reports process is already running fe := &fakeExec{started: true} @@ -2514,7 +2420,6 @@ func TestCrowdsecHandler_Start_AlreadyRunning(t *testing.T) { // TestCrowdsecHandler_Stop_WhenNotRunning verifies Stop behavior when process isn't running func TestCrowdsecHandler_Stop_WhenNotRunning(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) fe := &fakeExec{started: false} @@ -2539,7 +2444,6 @@ func TestCrowdsecHandler_Stop_WhenNotRunning(t *testing.T) { // TestCrowdsecHandler_BanIP_InvalidJSON verifies JSON binding for ban requests func TestCrowdsecHandler_BanIP_InvalidJSON(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() @@ -2558,7 +2462,6 @@ func TestCrowdsecHandler_BanIP_InvalidJSON(t *testing.T) { // TestCrowdsecHandler_UnbanIP_MissingParam verifies parameter validation func TestCrowdsecHandler_UnbanIP_MissingParam(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() @@ -2583,7 +2486,6 @@ func TestCrowdsecHandler_ListFiles_WalkError(t *testing.T) { } t.Parallel() - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() restrictedDir := filepath.Join(tmpDir, "restricted") @@ -2609,7 +2511,6 @@ func TestCrowdsecHandler_ListFiles_WalkError(t *testing.T) { // TestCrowdsecHandler_GetCachedPreset_InvalidSlug verifies slug validation func TestCrowdsecHandler_GetCachedPreset_InvalidSlug(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -2627,7 +2528,6 @@ func TestCrowdsecHandler_GetCachedPreset_InvalidSlug(t *testing.T) { // TestCrowdsecHandler_GetCachedPreset_CacheMiss verifies cache miss handling func TestCrowdsecHandler_GetCachedPreset_CacheMiss(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -2651,7 +2551,6 @@ func TestCrowdsecHandler_GetCachedPreset_CacheMiss(t *testing.T) { func TestCrowdsecHandler_RegisterBouncer_InvalidAPIKey(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) // Create mock executor that returns invalid API key format mockExec := &mockCmdExecutor{ @@ -2686,7 +2585,6 @@ exit 1 func TestCrowdsecHandler_RegisterBouncer_LAPIConnectionError(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) mockExec := &mockCmdExecutor{ output: []byte("Error: Cannot connect to LAPI\ncscli lapi status: connection refused\n"), @@ -2712,7 +2610,6 @@ func TestCrowdsecHandler_RegisterBouncer_LAPIConnectionError(t *testing.T) { func TestCrowdsecHandler_GetAcquisitionConfig_FileNotFound(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) r := gin.New() @@ -2735,7 +2632,6 @@ func TestCrowdsecHandler_GetAcquisitionConfig_FileNotFound(t *testing.T) { func TestCrowdsecHandler_GetAcquisitionConfig_ParseError(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) // This test verifies the handler returns content even if YAML is malformed // The handler doesn't parse YAML, it just reads the file content @@ -2758,7 +2654,6 @@ func TestCrowdsecHandler_GetAcquisitionConfig_ParseError(t *testing.T) { func TestCrowdsecHandler_ImportConfig_InvalidYAML(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -2788,7 +2683,6 @@ func TestCrowdsecHandler_ImportConfig_InvalidYAML(t *testing.T) { func TestCrowdsecHandler_ImportConfig_ReadError(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -2816,7 +2710,6 @@ func TestCrowdsecHandler_ImportConfig_ReadError(t *testing.T) { func TestCrowdsecHandler_ImportConfig_MissingRequiredFields(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir()) @@ -2848,7 +2741,6 @@ func TestCrowdsecHandler_ExportConfig_WriteError(t *testing.T) { } t.Parallel() - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() @@ -2880,7 +2772,6 @@ func TestCrowdsecHandler_ExportConfig_PermissionsDenied(t *testing.T) { } t.Parallel() - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() restrictedFile := filepath.Join(tmpDir, "restricted.conf") @@ -2910,7 +2801,6 @@ func TestCrowdsecHandler_ExportConfig_PermissionsDenied(t *testing.T) { func TestCrowdsecHandler_ExportConfig_SuccessValidation(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() @@ -2976,7 +2866,6 @@ common: func TestCrowdsecHandler_ListFiles_DirectoryNotExists(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) // Use explicitly non-existent directory nonExistentDir := filepath.Join(os.TempDir(), "crowdsec-test-nonexistent-"+t.Name()) @@ -3019,7 +2908,6 @@ func TestCrowdsecHandler_ListFiles_PermissionDenied(t *testing.T) { } t.Parallel() - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() restrictedDir := filepath.Join(tmpDir, "restricted") @@ -3050,7 +2938,6 @@ func TestCrowdsecHandler_ListFiles_PermissionDenied(t *testing.T) { func TestCrowdsecHandler_ListFiles_FilteringLogic(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() @@ -3103,7 +2990,6 @@ func TestCrowdsecHandler_ListFiles_FilteringLogic(t *testing.T) { // Test actual file operations to increase ExportConfig coverage func TestCrowdsecHandler_ExportConfig_MultipleDirectories(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() @@ -3163,7 +3049,6 @@ func TestCrowdsecHandler_ExportConfig_MultipleDirectories(t *testing.T) { // Test ListFiles with deeply nested structure func TestCrowdsecHandler_ListFiles_DeepNesting(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() @@ -3194,7 +3079,6 @@ func TestCrowdsecHandler_ListFiles_DeepNesting(t *testing.T) { // Test ImportConfig with actual file operations func TestCrowdsecHandler_ImportConfig_LargeFile(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -3235,7 +3119,6 @@ func TestCrowdsecHandler_ImportConfig_LargeFile(t *testing.T) { // Test Start with SecurityConfig creation func TestCrowdsecHandler_Start_CreatesSecurityConfig(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -3269,7 +3152,6 @@ func TestCrowdsecHandler_Start_CreatesSecurityConfig(t *testing.T) { // Test Stop updates existing SecurityConfig func TestCrowdsecHandler_Stop_UpdatesExistingConfig(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -3306,7 +3188,6 @@ func TestCrowdsecHandler_Stop_UpdatesExistingConfig(t *testing.T) { // Test WriteFile backup creation func TestCrowdsecHandler_WriteFile_BackupCreation(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() @@ -3350,7 +3231,6 @@ func TestCrowdsecHandler_WriteFile_BackupCreation(t *testing.T) { // Test ReadFile with path traversal protection func TestCrowdsecHandler_ReadFile_PathTraversal(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() @@ -3375,7 +3255,6 @@ func TestCrowdsecHandler_ReadFile_PathTraversal(t *testing.T) { // Test Status with config.yaml present func TestCrowdsecHandler_Status_WithConfigFile(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() @@ -3412,7 +3291,6 @@ func TestCrowdsecHandler_Status_WithConfigFile(t *testing.T) { // Test BanIP with reason func TestCrowdsecHandler_BanIP_WithReason(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) mockExec := &mockCmdExecutor{ output: []byte("Decision created"), @@ -3457,7 +3335,6 @@ func TestCrowdsecHandler_UpdateAcquisitionConfig_CreatesBackup(t *testing.T) { } t.Parallel() - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -3483,7 +3360,6 @@ func TestCrowdsecHandler_UpdateAcquisitionConfig_CreatesBackup(t *testing.T) { // Test Start when executor.Start fails func TestCrowdsecHandler_Start_ExecutorFailure(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) @@ -3522,7 +3398,6 @@ func TestCrowdsecHandler_Start_ExecutorFailure(t *testing.T) { // Test Start when LAPI doesn't become ready func TestCrowdsecHandler_Start_LAPINotReady(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) @@ -3557,7 +3432,6 @@ func TestCrowdsecHandler_Start_LAPINotReady(t *testing.T) { // Test ConsoleStatus when not enrolled func TestCrowdsecHandler_ConsoleStatus_NotEnrolled(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") t.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "true") @@ -3590,7 +3464,6 @@ func TestCrowdsecHandler_ConsoleStatus_NotEnrolled(t *testing.T) { // Test WriteFile with directory creation func TestCrowdsecHandler_WriteFile_DirectoryCreation(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() @@ -3624,7 +3497,6 @@ func TestCrowdsecHandler_WriteFile_DirectoryCreation(t *testing.T) { // Test GetLAPIDecisions with API errors func TestCrowdsecHandler_GetLAPIDecisions_APIError(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) @@ -3659,7 +3531,6 @@ func TestCrowdsecHandler_UpdateAcquisitionConfig_ReadError(t *testing.T) { } t.Parallel() - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -3682,7 +3553,6 @@ func TestCrowdsecHandler_UpdateAcquisitionConfig_ReadError(t *testing.T) { // Test CheckLAPIHealth with various failure modes func TestCrowdsecHandler_CheckLAPIHealth_Timeout(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) mockExec := &mockCmdExecutor{ output: []byte(""), @@ -3711,7 +3581,6 @@ func TestCrowdsecHandler_CheckLAPIHealth_Timeout(t *testing.T) { // Test ExportConfig with write errors func TestCrowdsecHandler_ExportConfig_EmptyDirectory(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() // Don't create any subdirectories @@ -3733,7 +3602,6 @@ func TestCrowdsecHandler_ExportConfig_EmptyDirectory(t *testing.T) { // Test ImportConfig with corrupted archive func TestCrowdsecHandler_ImportConfig_CorruptedArchive(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -3894,7 +3762,6 @@ func TestValidateAPIKeyFormat(t *testing.T) { // Security: Critical test to prevent API key leakage in logs (CWE-312, CWE-315, CWE-359). func TestLogBouncerKeyBanner_NoSecretExposure(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := OpenTestDB(t) tmpDir := t.TempDir() @@ -4493,7 +4360,6 @@ func TestEnsureBouncerRegistration_ConcurrentCalls(t *testing.T) { func TestValidateBouncerKey_BouncerExists(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) mockExec := &mockCmdExecutor{ output: []byte(`[{"name":"caddy-bouncer"}]`), @@ -4514,7 +4380,6 @@ func TestValidateBouncerKey_BouncerExists(t *testing.T) { func TestValidateBouncerKey_BouncerNotFound(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) mockExec := &mockCmdExecutor{ output: []byte(`[{"name":"some-other-bouncer"}]`), @@ -4533,7 +4398,6 @@ func TestValidateBouncerKey_BouncerNotFound(t *testing.T) { func TestValidateBouncerKey_EmptyOutput(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) mockExec := &mockCmdExecutor{ output: []byte(``), @@ -4552,7 +4416,6 @@ func TestValidateBouncerKey_EmptyOutput(t *testing.T) { func TestValidateBouncerKey_NullOutput(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) mockExec := &mockCmdExecutor{ output: []byte(`null`), @@ -4571,7 +4434,6 @@ func TestValidateBouncerKey_NullOutput(t *testing.T) { func TestValidateBouncerKey_CmdError(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) mockExec := &mockCmdExecutor{ output: nil, @@ -4590,7 +4452,6 @@ func TestValidateBouncerKey_CmdError(t *testing.T) { func TestValidateBouncerKey_InvalidJSON(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) mockExec := &mockCmdExecutor{ output: []byte(`not valid json`), @@ -4608,7 +4469,6 @@ func TestValidateBouncerKey_InvalidJSON(t *testing.T) { } func TestGetBouncerInfo_FromEnvVar(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("CROWDSEC_API_KEY", "test-api-key-12345678901234567890") mockExec := &mockCmdExecutor{ @@ -4636,7 +4496,6 @@ func TestGetBouncerInfo_FromEnvVar(t *testing.T) { } func TestGetBouncerInfo_NotRegistered(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("CROWDSEC_API_KEY", "test-api-key-12345678901234567890") mockExec := &mockCmdExecutor{ @@ -4663,7 +4522,6 @@ func TestGetBouncerInfo_NotRegistered(t *testing.T) { } func TestGetBouncerKey_FromEnvVar(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("CROWDSEC_API_KEY", "test-env-key-value-12345") h := &CrowdsecHandler{} @@ -4683,7 +4541,6 @@ func TestGetBouncerKey_FromEnvVar(t *testing.T) { } func TestGetKeyStatus_EnvKeyValid(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("CROWDSEC_API_KEY", "test-api-key-12345678901234567890") h := &CrowdsecHandler{} @@ -4703,7 +4560,6 @@ func TestGetKeyStatus_EnvKeyValid(t *testing.T) { } func TestGetKeyStatus_EnvKeyRejected(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("CROWDSEC_API_KEY", "rejected-key-123456789012345") h := &CrowdsecHandler{ diff --git a/backend/internal/api/handlers/crowdsec_lapi_test.go b/backend/internal/api/handlers/crowdsec_lapi_test.go index 58e7a97be..28f4f3b30 100644 --- a/backend/internal/api/handlers/crowdsec_lapi_test.go +++ b/backend/internal/api/handlers/crowdsec_lapi_test.go @@ -12,7 +12,6 @@ import ( ) func TestGetLAPIDecisions_FallbackToCscli(t *testing.T) { - gin.SetMode(gin.TestMode) router := gin.New() // Create handler with mock executor @@ -40,7 +39,6 @@ func TestGetLAPIDecisions_FallbackToCscli(t *testing.T) { } func TestGetLAPIDecisions_EmptyResponse(t *testing.T) { - gin.SetMode(gin.TestMode) router := gin.New() // Create handler with mock executor that returns empty array @@ -67,7 +65,6 @@ func TestGetLAPIDecisions_EmptyResponse(t *testing.T) { } func TestCheckLAPIHealth_Handler(t *testing.T) { - gin.SetMode(gin.TestMode) router := gin.New() handler := &CrowdsecHandler{ diff --git a/backend/internal/api/handlers/crowdsec_presets_handler_test.go b/backend/internal/api/handlers/crowdsec_presets_handler_test.go index 2947eaa60..cf0e734a0 100644 --- a/backend/internal/api/handlers/crowdsec_presets_handler_test.go +++ b/backend/internal/api/handlers/crowdsec_presets_handler_test.go @@ -46,7 +46,6 @@ func makePresetTar(t *testing.T, files map[string]string) []byte { } func TestListPresetsIncludesCacheAndIndex(t *testing.T) { - gin.SetMode(gin.TestMode) cache, err := crowdsec.NewHubCache(t.TempDir(), time.Hour) require.NoError(t, err) _, err = cache.Store(context.Background(), "crowdsecurity/demo", "etag1", "hub", "preview", []byte("archive")) @@ -92,7 +91,6 @@ func TestListPresetsIncludesCacheAndIndex(t *testing.T) { } func TestPullPresetHandlerSuccess(t *testing.T) { - gin.SetMode(gin.TestMode) cache, err := crowdsec.NewHubCache(t.TempDir(), time.Hour) require.NoError(t, err) dataDir := filepath.Join(t.TempDir(), "crowdsec") @@ -132,7 +130,6 @@ func TestPullPresetHandlerSuccess(t *testing.T) { } func TestApplyPresetHandlerAudits(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.CrowdsecPresetEvent{})) @@ -186,7 +183,6 @@ func TestApplyPresetHandlerAudits(t *testing.T) { } func TestPullPresetHandlerHubError(t *testing.T) { - gin.SetMode(gin.TestMode) cache, err := crowdsec.NewHubCache(t.TempDir(), time.Hour) require.NoError(t, err) @@ -213,7 +209,6 @@ func TestPullPresetHandlerHubError(t *testing.T) { } func TestPullPresetHandlerTimeout(t *testing.T) { - gin.SetMode(gin.TestMode) cache, err := crowdsec.NewHubCache(t.TempDir(), time.Hour) require.NoError(t, err) @@ -241,7 +236,6 @@ func TestPullPresetHandlerTimeout(t *testing.T) { } func TestGetCachedPresetNotFound(t *testing.T) { - gin.SetMode(gin.TestMode) cache, err := crowdsec.NewHubCache(t.TempDir(), time.Hour) require.NoError(t, err) @@ -260,7 +254,6 @@ func TestGetCachedPresetNotFound(t *testing.T) { } func TestGetCachedPresetServiceUnavailable(t *testing.T) { - gin.SetMode(gin.TestMode) h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) h.Hub = &crowdsec.HubService{} @@ -277,7 +270,6 @@ func TestGetCachedPresetServiceUnavailable(t *testing.T) { } func TestApplyPresetHandlerBackupFailure(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.CrowdsecPresetEvent{})) @@ -325,7 +317,6 @@ func TestApplyPresetHandlerBackupFailure(t *testing.T) { } func TestListPresetsMergesCuratedAndHub(t *testing.T) { - gin.SetMode(gin.TestMode) hub := crowdsec.NewHubService(nil, nil, t.TempDir()) hub.HubBaseURL = "http://hub.example" @@ -375,7 +366,6 @@ func TestListPresetsMergesCuratedAndHub(t *testing.T) { } func TestGetCachedPresetSuccess(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") cache, err := crowdsec.NewHubCache(t.TempDir(), time.Hour) require.NoError(t, err) @@ -403,7 +393,6 @@ func TestGetCachedPresetSuccess(t *testing.T) { } func TestGetCachedPresetSlugRequired(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") cache, err := crowdsec.NewHubCache(t.TempDir(), time.Hour) require.NoError(t, err) @@ -424,7 +413,6 @@ func TestGetCachedPresetSlugRequired(t *testing.T) { } func TestGetCachedPresetPreviewError(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") cacheDir := t.TempDir() cache, err := crowdsec.NewHubCache(cacheDir, time.Hour) @@ -451,7 +439,6 @@ func TestGetCachedPresetPreviewError(t *testing.T) { } func TestPullCuratedPresetSkipsHub(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") // Setup handler with a hub service that would fail if called @@ -489,7 +476,6 @@ func TestPullCuratedPresetSkipsHub(t *testing.T) { } func TestApplyCuratedPresetSkipsHub(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") db := OpenTestDB(t) diff --git a/backend/internal/api/handlers/crowdsec_pull_apply_integration_test.go b/backend/internal/api/handlers/crowdsec_pull_apply_integration_test.go index e0fcdc076..f714e42d2 100644 --- a/backend/internal/api/handlers/crowdsec_pull_apply_integration_test.go +++ b/backend/internal/api/handlers/crowdsec_pull_apply_integration_test.go @@ -24,7 +24,6 @@ import ( // TestPullThenApplyIntegration tests the complete pull→apply workflow from the user's perspective. // This reproduces the scenario where a user pulls a preset and then tries to apply it. func TestPullThenApplyIntegration(t *testing.T) { - gin.SetMode(gin.TestMode) // Setup cacheDir := t.TempDir() @@ -111,7 +110,6 @@ func TestPullThenApplyIntegration(t *testing.T) { // TestApplyWithoutPullReturnsProperError verifies the error message when applying without pulling first. func TestApplyWithoutPullReturnsProperError(t *testing.T) { - gin.SetMode(gin.TestMode) cacheDir := t.TempDir() dataDir := t.TempDir() @@ -155,7 +153,6 @@ func TestApplyWithoutPullReturnsProperError(t *testing.T) { } func TestApplyRollbackWhenCacheMissingAndRepullFails(t *testing.T) { - gin.SetMode(gin.TestMode) cacheDir := t.TempDir() dataRoot := t.TempDir() diff --git a/backend/internal/api/handlers/crowdsec_state_sync_test.go b/backend/internal/api/handlers/crowdsec_state_sync_test.go index 6b50810b9..a7cb29cd7 100644 --- a/backend/internal/api/handlers/crowdsec_state_sync_test.go +++ b/backend/internal/api/handlers/crowdsec_state_sync_test.go @@ -14,7 +14,6 @@ import ( // TestStartSyncsSettingsTable verifies that Start() updates the settings table. func TestStartSyncsSettingsTable(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) // Migrate both SecurityConfig and Setting tables @@ -78,7 +77,6 @@ func TestStartSyncsSettingsTable(t *testing.T) { // TestStopSyncsSettingsTable verifies that Stop() updates the settings table. func TestStopSyncsSettingsTable(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) // Migrate both SecurityConfig and Setting tables @@ -147,7 +145,6 @@ func TestStopSyncsSettingsTable(t *testing.T) { // TestStartAndStopStateConsistency verifies consistent state across Start/Stop cycles. func TestStartAndStopStateConsistency(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{})) @@ -219,7 +216,6 @@ func TestStartAndStopStateConsistency(t *testing.T) { // TestExistingSettingIsUpdated verifies that an existing setting is updated, not duplicated. func TestExistingSettingIsUpdated(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{})) @@ -293,7 +289,6 @@ func (f *fakeFailingExec) Status(ctx context.Context, configDir string) (running // TestStartFailureRevertsSettings verifies that a failed Start reverts the settings. func TestStartFailureRevertsSettings(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{})) @@ -330,7 +325,6 @@ func TestStartFailureRevertsSettings(t *testing.T) { // TestStatusResponseFormat verifies the status endpoint response format. func TestStatusResponseFormat(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{})) diff --git a/backend/internal/api/handlers/crowdsec_stop_lapi_test.go b/backend/internal/api/handlers/crowdsec_stop_lapi_test.go index 01f1cccb1..b305b0371 100644 --- a/backend/internal/api/handlers/crowdsec_stop_lapi_test.go +++ b/backend/internal/api/handlers/crowdsec_stop_lapi_test.go @@ -51,7 +51,6 @@ func createTestSecurityService(t *testing.T, db *gorm.DB) *services.SecurityServ // TestCrowdsecHandler_Stop_Success tests the Stop handler with successful execution func TestCrowdsecHandler_Stop_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{})) @@ -97,7 +96,6 @@ func TestCrowdsecHandler_Stop_Success(t *testing.T) { // TestCrowdsecHandler_Stop_Error tests the Stop handler with an execution error func TestCrowdsecHandler_Stop_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{})) @@ -123,7 +121,6 @@ func TestCrowdsecHandler_Stop_Error(t *testing.T) { // TestCrowdsecHandler_Stop_NoSecurityConfig tests Stop when there's no existing SecurityConfig func TestCrowdsecHandler_Stop_NoSecurityConfig(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{})) @@ -152,10 +149,6 @@ func TestCrowdsecHandler_Stop_NoSecurityConfig(t *testing.T) { // TestGetLAPIDecisions_WithMockServer tests GetLAPIDecisions with a mock LAPI server func TestGetLAPIDecisions_WithMockServer(t *testing.T) { - // Use permissive validator for testing with mock server on random port - orig := validateCrowdsecLAPIBaseURLFunc - validateCrowdsecLAPIBaseURLFunc = permissiveLAPIURLValidator - defer func() { validateCrowdsecLAPIBaseURLFunc = orig }() // Create a mock LAPI server mockLAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -165,7 +158,6 @@ func TestGetLAPIDecisions_WithMockServer(t *testing.T) { })) defer mockLAPI.Close() - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -179,6 +171,7 @@ func TestGetLAPIDecisions_WithMockServer(t *testing.T) { Security: secSvc, CmdExec: &mockCommandExecutor{}, DataDir: t.TempDir(), + validateLAPIURL: permissiveLAPIURLValidator, } r := gin.New() @@ -202,10 +195,6 @@ func TestGetLAPIDecisions_WithMockServer(t *testing.T) { // TestGetLAPIDecisions_Unauthorized tests GetLAPIDecisions when LAPI returns 401 func TestGetLAPIDecisions_Unauthorized(t *testing.T) { - // Use permissive validator for testing with mock server on random port - orig := validateCrowdsecLAPIBaseURLFunc - validateCrowdsecLAPIBaseURLFunc = permissiveLAPIURLValidator - defer func() { validateCrowdsecLAPIBaseURLFunc = orig }() // Create a mock LAPI server that returns 401 mockLAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -213,7 +202,6 @@ func TestGetLAPIDecisions_Unauthorized(t *testing.T) { })) defer mockLAPI.Close() - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -226,6 +214,7 @@ func TestGetLAPIDecisions_Unauthorized(t *testing.T) { Security: secSvc, CmdExec: &mockCommandExecutor{}, DataDir: t.TempDir(), + validateLAPIURL: permissiveLAPIURLValidator, } r := gin.New() @@ -240,10 +229,6 @@ func TestGetLAPIDecisions_Unauthorized(t *testing.T) { // TestGetLAPIDecisions_NullResponse tests GetLAPIDecisions when LAPI returns null func TestGetLAPIDecisions_NullResponse(t *testing.T) { - // Use permissive validator for testing with mock server on random port - orig := validateCrowdsecLAPIBaseURLFunc - validateCrowdsecLAPIBaseURLFunc = permissiveLAPIURLValidator - defer func() { validateCrowdsecLAPIBaseURLFunc = orig }() mockLAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -252,7 +237,6 @@ func TestGetLAPIDecisions_NullResponse(t *testing.T) { })) defer mockLAPI.Close() - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -265,6 +249,7 @@ func TestGetLAPIDecisions_NullResponse(t *testing.T) { Security: secSvc, CmdExec: &mockCommandExecutor{}, DataDir: t.TempDir(), + validateLAPIURL: permissiveLAPIURLValidator, } r := gin.New() @@ -292,7 +277,6 @@ func TestGetLAPIDecisions_NonJSONContentType(t *testing.T) { })) defer mockLAPI.Close() - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -320,10 +304,6 @@ func TestGetLAPIDecisions_NonJSONContentType(t *testing.T) { // TestCheckLAPIHealth_WithMockServer tests CheckLAPIHealth with a healthy LAPI func TestCheckLAPIHealth_WithMockServer(t *testing.T) { - // Use permissive validator for testing with mock server on random port - orig := validateCrowdsecLAPIBaseURLFunc - validateCrowdsecLAPIBaseURLFunc = permissiveLAPIURLValidator - defer func() { validateCrowdsecLAPIBaseURLFunc = orig }() mockLAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/health" { @@ -335,7 +315,6 @@ func TestCheckLAPIHealth_WithMockServer(t *testing.T) { })) defer mockLAPI.Close() - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -348,6 +327,7 @@ func TestCheckLAPIHealth_WithMockServer(t *testing.T) { Security: secSvc, CmdExec: &mockCommandExecutor{}, DataDir: t.TempDir(), + validateLAPIURL: permissiveLAPIURLValidator, } r := gin.New() @@ -368,10 +348,6 @@ func TestCheckLAPIHealth_WithMockServer(t *testing.T) { // TestCheckLAPIHealth_FallbackToDecisions tests the fallback to /v1/decisions endpoint // when the primary /health endpoint is unreachable func TestCheckLAPIHealth_FallbackToDecisions(t *testing.T) { - // Use permissive validator for testing with mock server on random port - orig := validateCrowdsecLAPIBaseURLFunc - validateCrowdsecLAPIBaseURLFunc = permissiveLAPIURLValidator - defer func() { validateCrowdsecLAPIBaseURLFunc = orig }() // Create a mock server that only responds to /v1/decisions, not /health mockLAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -385,7 +361,6 @@ func TestCheckLAPIHealth_FallbackToDecisions(t *testing.T) { })) defer mockLAPI.Close() - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -398,6 +373,7 @@ func TestCheckLAPIHealth_FallbackToDecisions(t *testing.T) { Security: secSvc, CmdExec: &mockCommandExecutor{}, DataDir: t.TempDir(), + validateLAPIURL: permissiveLAPIURLValidator, } r := gin.New() diff --git a/backend/internal/api/handlers/crowdsec_wave3_test.go b/backend/internal/api/handlers/crowdsec_wave3_test.go index 4d719f9c6..c9ddac69c 100644 --- a/backend/internal/api/handlers/crowdsec_wave3_test.go +++ b/backend/internal/api/handlers/crowdsec_wave3_test.go @@ -47,7 +47,6 @@ func TestReadAcquisitionConfig_ErrorsAndSuccess(t *testing.T) { } func TestCrowdsec_AcquisitionEndpoints_InvalidConfiguredPath(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("CHARON_CROWDSEC_ACQUIS_PATH", "relative/path.yaml") h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir()) @@ -68,7 +67,6 @@ func TestCrowdsec_AcquisitionEndpoints_InvalidConfiguredPath(t *testing.T) { } func TestCrowdsec_GetBouncerKey_NotConfigured(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("CROWDSEC_API_KEY", "") t.Setenv("CROWDSEC_BOUNCER_API_KEY", "") t.Setenv("CERBERUS_SECURITY_CROWDSEC_API_KEY", "") diff --git a/backend/internal/api/handlers/crowdsec_wave5_test.go b/backend/internal/api/handlers/crowdsec_wave5_test.go index b71df08e3..98ffa8f15 100644 --- a/backend/internal/api/handlers/crowdsec_wave5_test.go +++ b/backend/internal/api/handlers/crowdsec_wave5_test.go @@ -27,7 +27,6 @@ func TestCrowdsecWave5_ReadAcquisitionConfig_InvalidFilenameBranch(t *testing.T) } func TestCrowdsecWave5_GetLAPIDecisions_Unauthorized(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -36,17 +35,11 @@ func TestCrowdsecWave5_GetLAPIDecisions_Unauthorized(t *testing.T) { })) t.Cleanup(server.Close) - original := validateCrowdsecLAPIBaseURLFunc - validateCrowdsecLAPIBaseURLFunc = func(raw string) (*url.URL, error) { - return url.Parse(raw) - } - t.Cleanup(func() { - validateCrowdsecLAPIBaseURLFunc = original - }) require.NoError(t, db.Create(&models.SecurityConfig{UUID: "default", CrowdSecAPIURL: server.URL}).Error) h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", tmpDir) + h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) } r := gin.New() g := r.Group("/api/v1") h.RegisterRoutes(g) @@ -60,7 +53,6 @@ func TestCrowdsecWave5_GetLAPIDecisions_Unauthorized(t *testing.T) { } func TestCrowdsecWave5_GetLAPIDecisions_NonJSONContentTypeFallsBack(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupCrowdDB(t) tmpDir := t.TempDir() @@ -71,17 +63,11 @@ func TestCrowdsecWave5_GetLAPIDecisions_NonJSONContentTypeFallsBack(t *testing.T })) t.Cleanup(server.Close) - original := validateCrowdsecLAPIBaseURLFunc - validateCrowdsecLAPIBaseURLFunc = func(raw string) (*url.URL, error) { - return url.Parse(raw) - } - t.Cleanup(func() { - validateCrowdsecLAPIBaseURLFunc = original - }) require.NoError(t, db.Create(&models.SecurityConfig{UUID: "default", CrowdSecAPIURL: server.URL}).Error) h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", tmpDir) + h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) } h.CmdExec = &mockCmdExecutor{output: []byte("[]"), err: nil} r := gin.New() g := r.Group("/api/v1") @@ -96,7 +82,6 @@ func TestCrowdsecWave5_GetLAPIDecisions_NonJSONContentTypeFallsBack(t *testing.T } func TestCrowdsecWave5_GetBouncerInfo_And_GetBouncerKey_FileSource(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("CROWDSEC_BOUNCER_API_KEY", "") t.Setenv("CERBERUS_SECURITY_CROWDSEC_API_KEY", "") t.Setenv("CHARON_SECURITY_CROWDSEC_API_KEY", "") @@ -105,6 +90,7 @@ func TestCrowdsecWave5_GetBouncerInfo_And_GetBouncerKey_FileSource(t *testing.T) tmpDir := t.TempDir() h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", tmpDir) + h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) } keyPath := h.bouncerKeyPath() require.NoError(t, os.MkdirAll(filepath.Dir(keyPath), 0o750)) require.NoError(t, os.WriteFile(keyPath, []byte("abcdefghijklmnop1234567890"), 0o600)) diff --git a/backend/internal/api/handlers/crowdsec_wave6_test.go b/backend/internal/api/handlers/crowdsec_wave6_test.go index 48571053c..7c697c1da 100644 --- a/backend/internal/api/handlers/crowdsec_wave6_test.go +++ b/backend/internal/api/handlers/crowdsec_wave6_test.go @@ -17,7 +17,6 @@ func TestCrowdsecWave6_BouncerKeyPath_UsesEnvFallback(t *testing.T) { } func TestCrowdsecWave6_GetBouncerInfo_NoneSource(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("CROWDSEC_API_KEY", "") t.Setenv("CROWDSEC_BOUNCER_API_KEY", "") t.Setenv("CERBERUS_SECURITY_CROWDSEC_API_KEY", "") @@ -40,7 +39,6 @@ func TestCrowdsecWave6_GetBouncerInfo_NoneSource(t *testing.T) { } func TestCrowdsecWave6_GetKeyStatus_NoKeyConfiguredMessage(t *testing.T) { - gin.SetMode(gin.TestMode) t.Setenv("CROWDSEC_API_KEY", "") t.Setenv("CROWDSEC_BOUNCER_API_KEY", "") t.Setenv("CERBERUS_SECURITY_CROWDSEC_API_KEY", "") diff --git a/backend/internal/api/handlers/crowdsec_wave7_test.go b/backend/internal/api/handlers/crowdsec_wave7_test.go index 3211de9cf..e5f0d95f6 100644 --- a/backend/internal/api/handlers/crowdsec_wave7_test.go +++ b/backend/internal/api/handlers/crowdsec_wave7_test.go @@ -28,7 +28,6 @@ func TestCrowdsecWave7_ReadAcquisitionConfig_ReadErrorOnDirectory(t *testing.T) } func TestCrowdsecWave7_Start_CreateSecurityConfigFailsOnReadOnlyDB(t *testing.T) { - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() dbPath := filepath.Join(tmpDir, "crowdsec-readonly.db") diff --git a/backend/internal/api/handlers/db_health_handler_test.go b/backend/internal/api/handlers/db_health_handler_test.go index d76b17fca..47cbfd3d6 100644 --- a/backend/internal/api/handlers/db_health_handler_test.go +++ b/backend/internal/api/handlers/db_health_handler_test.go @@ -36,7 +36,6 @@ func createTestSQLiteDB(dbPath string) error { } func TestDBHealthHandler_Check_Healthy(t *testing.T) { - gin.SetMode(gin.TestMode) // Create in-memory database db, err := database.Connect("file::memory:?cache=shared") @@ -65,7 +64,6 @@ func TestDBHealthHandler_Check_Healthy(t *testing.T) { } func TestDBHealthHandler_Check_WithBackupService(t *testing.T) { - gin.SetMode(gin.TestMode) // Setup temp dirs for backup service tmpDir := t.TempDir() @@ -116,7 +114,6 @@ func TestDBHealthHandler_Check_WithBackupService(t *testing.T) { } func TestDBHealthHandler_Check_WALMode(t *testing.T) { - gin.SetMode(gin.TestMode) // Create file-based database to test WAL mode tmpDir := t.TempDir() @@ -145,7 +142,6 @@ func TestDBHealthHandler_Check_WALMode(t *testing.T) { } func TestDBHealthHandler_ResponseJSONTags(t *testing.T) { - gin.SetMode(gin.TestMode) db, err := database.Connect("file::memory:?cache=shared") require.NoError(t, err) @@ -200,7 +196,6 @@ func TestNewDBHealthHandler(t *testing.T) { // Phase 1 & 3: Critical coverage tests func TestDBHealthHandler_Check_CorruptedDatabase(t *testing.T) { - gin.SetMode(gin.TestMode) // Create a file-based database and corrupt it tmpDir := t.TempDir() @@ -252,7 +247,6 @@ func TestDBHealthHandler_Check_CorruptedDatabase(t *testing.T) { } func TestDBHealthHandler_Check_BackupServiceError(t *testing.T) { - gin.SetMode(gin.TestMode) // Create database db, err := database.Connect("file::memory:?cache=shared") @@ -294,7 +288,6 @@ func TestDBHealthHandler_Check_BackupServiceError(t *testing.T) { } func TestDBHealthHandler_Check_BackupTimeZero(t *testing.T) { - gin.SetMode(gin.TestMode) // Create database db, err := database.Connect("file::memory:?cache=shared") diff --git a/backend/internal/api/handlers/dns_detection_handler_test.go b/backend/internal/api/handlers/dns_detection_handler_test.go index 61d02c996..fafa91a70 100644 --- a/backend/internal/api/handlers/dns_detection_handler_test.go +++ b/backend/internal/api/handlers/dns_detection_handler_test.go @@ -51,7 +51,6 @@ func TestNewDNSDetectionHandler(t *testing.T) { } func TestDetect_Success(t *testing.T) { - gin.SetMode(gin.TestMode) mockService := new(mockDNSDetectionService) handler := NewDNSDetectionHandler(mockService) @@ -177,7 +176,6 @@ func TestDetect_Success(t *testing.T) { } func TestDetect_ValidationErrors(t *testing.T) { - gin.SetMode(gin.TestMode) mockService := new(mockDNSDetectionService) handler := NewDNSDetectionHandler(mockService) @@ -216,7 +214,6 @@ func TestDetect_ValidationErrors(t *testing.T) { } func TestDetect_ServiceError(t *testing.T) { - gin.SetMode(gin.TestMode) mockService := new(mockDNSDetectionService) handler := NewDNSDetectionHandler(mockService) @@ -246,7 +243,6 @@ func TestDetect_ServiceError(t *testing.T) { } func TestGetPatterns(t *testing.T) { - gin.SetMode(gin.TestMode) mockService := new(mockDNSDetectionService) handler := NewDNSDetectionHandler(mockService) @@ -287,7 +283,6 @@ func TestGetPatterns(t *testing.T) { } func TestDetect_WildcardDomain(t *testing.T) { - gin.SetMode(gin.TestMode) mockService := new(mockDNSDetectionService) handler := NewDNSDetectionHandler(mockService) @@ -327,7 +322,6 @@ func TestDetect_WildcardDomain(t *testing.T) { } func TestDetect_LowConfidence(t *testing.T) { - gin.SetMode(gin.TestMode) mockService := new(mockDNSDetectionService) handler := NewDNSDetectionHandler(mockService) @@ -368,7 +362,6 @@ func TestDetect_LowConfidence(t *testing.T) { } func TestDetect_DNSLookupError(t *testing.T) { - gin.SetMode(gin.TestMode) mockService := new(mockDNSDetectionService) handler := NewDNSDetectionHandler(mockService) @@ -438,7 +431,6 @@ func TestDetectRequest_Binding(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gin.SetMode(gin.TestMode) c, _ := gin.CreateTestContext(httptest.NewRecorder()) c.Request = httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(tt.body)) c.Request.Header.Set("Content-Type", "application/json") diff --git a/backend/internal/api/handlers/dns_provider_handler_test.go b/backend/internal/api/handlers/dns_provider_handler_test.go index 89d24b796..8719627ef 100644 --- a/backend/internal/api/handlers/dns_provider_handler_test.go +++ b/backend/internal/api/handlers/dns_provider_handler_test.go @@ -106,7 +106,6 @@ func (m *MockDNSProviderService) GetDecryptedCredentials(ctx context.Context, id } func setupDNSProviderTestRouter() (*gin.Engine, *MockDNSProviderService) { - gin.SetMode(gin.TestMode) router := gin.New() mockService := new(MockDNSProviderService) handler := NewDNSProviderHandler(mockService) diff --git a/backend/internal/api/handlers/docker_handler_test.go b/backend/internal/api/handlers/docker_handler_test.go index 99a297fd7..73cc811d2 100644 --- a/backend/internal/api/handlers/docker_handler_test.go +++ b/backend/internal/api/handlers/docker_handler_test.go @@ -41,7 +41,6 @@ func (f *fakeRemoteServerService) GetByUUID(uuidStr string) (*models.RemoteServe } func TestDockerHandler_ListContainers_InvalidHostRejected(t *testing.T) { - gin.SetMode(gin.TestMode) router := gin.New() dockerSvc := &fakeDockerService{} @@ -60,7 +59,6 @@ func TestDockerHandler_ListContainers_InvalidHostRejected(t *testing.T) { } func TestDockerHandler_ListContainers_DockerUnavailableMappedTo503(t *testing.T) { - gin.SetMode(gin.TestMode) router := gin.New() dockerSvc := &fakeDockerService{err: services.NewDockerUnavailableError(errors.New("no docker socket"), "Local Docker socket is mounted but not accessible by current process")} @@ -82,7 +80,6 @@ func TestDockerHandler_ListContainers_DockerUnavailableMappedTo503(t *testing.T) } func TestDockerHandler_ListContainers_ServerIDResolvesToTCPHost(t *testing.T) { - gin.SetMode(gin.TestMode) router := gin.New() dockerSvc := &fakeDockerService{ret: []services.DockerContainer{}} @@ -103,7 +100,6 @@ func TestDockerHandler_ListContainers_ServerIDResolvesToTCPHost(t *testing.T) { } func TestDockerHandler_ListContainers_ServerIDNotFoundReturns404(t *testing.T) { - gin.SetMode(gin.TestMode) router := gin.New() dockerSvc := &fakeDockerService{} @@ -125,7 +121,6 @@ func TestDockerHandler_ListContainers_ServerIDNotFoundReturns404(t *testing.T) { func TestDockerHandler_ListContainers_Local(t *testing.T) { // Test local/default docker connection (empty host parameter) - gin.SetMode(gin.TestMode) router := gin.New() dockerSvc := &fakeDockerService{ @@ -163,7 +158,6 @@ func TestDockerHandler_ListContainers_Local(t *testing.T) { func TestDockerHandler_ListContainers_RemoteServerSuccess(t *testing.T) { // Test successful remote server connection via server_id - gin.SetMode(gin.TestMode) router := gin.New() dockerSvc := &fakeDockerService{ @@ -203,7 +197,6 @@ func TestDockerHandler_ListContainers_RemoteServerSuccess(t *testing.T) { func TestDockerHandler_ListContainers_RemoteServerNotFound(t *testing.T) { // Test server_id that doesn't exist in database - gin.SetMode(gin.TestMode) router := gin.New() dockerSvc := &fakeDockerService{} @@ -226,7 +219,6 @@ func TestDockerHandler_ListContainers_RemoteServerNotFound(t *testing.T) { func TestDockerHandler_ListContainers_InvalidHost(t *testing.T) { // Test SSRF protection: reject arbitrary host values - gin.SetMode(gin.TestMode) router := gin.New() dockerSvc := &fakeDockerService{} @@ -289,7 +281,6 @@ func TestDockerHandler_ListContainers_DockerUnavailable(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gin.SetMode(gin.TestMode) router := gin.New() dockerSvc := &fakeDockerService{err: tt.err} @@ -340,7 +331,6 @@ func TestDockerHandler_ListContainers_GenericError(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gin.SetMode(gin.TestMode) router := gin.New() dockerSvc := &fakeDockerService{err: tt.err} @@ -362,7 +352,6 @@ func TestDockerHandler_ListContainers_GenericError(t *testing.T) { } func TestDockerHandler_ListContainers_503FallbackDetailsWhenEmpty(t *testing.T) { - gin.SetMode(gin.TestMode) router := gin.New() dockerSvc := &fakeDockerService{err: services.NewDockerUnavailableError(errors.New("socket error"))} @@ -382,7 +371,6 @@ func TestDockerHandler_ListContainers_503FallbackDetailsWhenEmpty(t *testing.T) } func TestDockerHandler_ListContainers_503DetailsWithGroupGuidance(t *testing.T) { - gin.SetMode(gin.TestMode) router := gin.New() groupDetails := `Local Docker socket is mounted but not accessible by current process (uid=1000 gid=1000). Process groups (1000) do not include socket gid 988; run container with matching supplemental group (e.g., --group-add 988 or compose group_add: ["988"]).` diff --git a/backend/internal/api/handlers/emergency_handler_test.go b/backend/internal/api/handlers/emergency_handler_test.go index 4106577a9..77c75b787 100644 --- a/backend/internal/api/handlers/emergency_handler_test.go +++ b/backend/internal/api/handlers/emergency_handler_test.go @@ -87,7 +87,6 @@ func setupEmergencyTestDB(t *testing.T) *gorm.DB { } func setupEmergencyRouter(handler *EmergencyHandler) *gin.Engine { - gin.SetMode(gin.TestMode) router := gin.New() _ = router.SetTrustedProxies(nil) router.POST("/api/v1/emergency/security-reset", handler.SecurityReset) @@ -385,7 +384,6 @@ func TestEmergencySecurityReset_MiddlewarePrevalidatedBypass(t *testing.T) { db := setupEmergencyTestDB(t) handler := NewEmergencyHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() router.POST("/api/v1/emergency/security-reset", func(c *gin.Context) { c.Set("emergency_bypass", true) @@ -407,7 +405,6 @@ func TestEmergencySecurityReset_MiddlewareBypass_ResetFailure(t *testing.T) { require.NoError(t, err) require.NoError(t, stdDB.Close()) - gin.SetMode(gin.TestMode) router := gin.New() router.POST("/api/v1/emergency/security-reset", func(c *gin.Context) { c.Set("emergency_bypass", true) @@ -475,7 +472,6 @@ func TestGenerateToken_Success(t *testing.T) { handler := NewEmergencyTokenHandler(tokenService) defer handler.Close() - gin.SetMode(gin.TestMode) router := gin.New() router.POST("/api/v1/emergency/token", func(c *gin.Context) { c.Set("role", "admin") @@ -504,7 +500,6 @@ func TestGenerateToken_AdminRequired(t *testing.T) { handler := NewEmergencyTokenHandler(tokenService) defer handler.Close() - gin.SetMode(gin.TestMode) router := gin.New() router.POST("/api/v1/emergency/token", func(c *gin.Context) { // No role set - simulating non-admin user @@ -527,7 +522,6 @@ func TestGenerateToken_InvalidExpirationDays(t *testing.T) { handler := NewEmergencyTokenHandler(tokenService) defer handler.Close() - gin.SetMode(gin.TestMode) router := gin.New() router.POST("/api/v1/emergency/token", func(c *gin.Context) { c.Set("role", "admin") @@ -554,7 +548,6 @@ func TestGetTokenStatus_Success(t *testing.T) { // Generate a token first _, _ = tokenService.Generate(services.GenerateRequest{ExpirationDays: 30}) - gin.SetMode(gin.TestMode) router := gin.New() router.GET("/api/v1/emergency/token/status", func(c *gin.Context) { c.Set("role", "admin") @@ -581,7 +574,6 @@ func TestGetTokenStatus_AdminRequired(t *testing.T) { handler := NewEmergencyTokenHandler(tokenService) defer handler.Close() - gin.SetMode(gin.TestMode) router := gin.New() router.GET("/api/v1/emergency/token/status", handler.GetTokenStatus) @@ -602,7 +594,6 @@ func TestRevokeToken_Success(t *testing.T) { // Generate a token first _, _ = tokenService.Generate(services.GenerateRequest{ExpirationDays: 30}) - gin.SetMode(gin.TestMode) router := gin.New() router.DELETE("/api/v1/emergency/token", func(c *gin.Context) { c.Set("role", "admin") @@ -624,7 +615,6 @@ func TestRevokeToken_AdminRequired(t *testing.T) { handler := NewEmergencyTokenHandler(tokenService) defer handler.Close() - gin.SetMode(gin.TestMode) router := gin.New() router.DELETE("/api/v1/emergency/token", handler.RevokeToken) @@ -645,7 +635,6 @@ func TestUpdateTokenExpiration_Success(t *testing.T) { // Generate a token first _, _ = tokenService.Generate(services.GenerateRequest{ExpirationDays: 30}) - gin.SetMode(gin.TestMode) router := gin.New() router.PATCH("/api/v1/emergency/token/expiration", func(c *gin.Context) { c.Set("role", "admin") @@ -669,7 +658,6 @@ func TestUpdateTokenExpiration_AdminRequired(t *testing.T) { handler := NewEmergencyTokenHandler(tokenService) defer handler.Close() - gin.SetMode(gin.TestMode) router := gin.New() router.PATCH("/api/v1/emergency/token/expiration", handler.UpdateTokenExpiration) @@ -689,7 +677,6 @@ func TestUpdateTokenExpiration_InvalidDays(t *testing.T) { handler := NewEmergencyTokenHandler(tokenService) defer handler.Close() - gin.SetMode(gin.TestMode) router := gin.New() router.PATCH("/api/v1/emergency/token/expiration", func(c *gin.Context) { c.Set("role", "admin") diff --git a/backend/internal/api/handlers/encryption_handler_test.go b/backend/internal/api/handlers/encryption_handler_test.go index d6addbe96..7e7479cd9 100644 --- a/backend/internal/api/handlers/encryption_handler_test.go +++ b/backend/internal/api/handlers/encryption_handler_test.go @@ -40,7 +40,6 @@ func setupEncryptionTestDB(t *testing.T) *gorm.DB { } func setupEncryptionTestRouter(handler *EncryptionHandler, isAdmin bool) *gin.Engine { - gin.SetMode(gin.TestMode) router := gin.New() // Mock admin middleware - matches production auth middleware key names @@ -558,7 +557,6 @@ func TestEncryptionHandler_IntegrationFlow(t *testing.T) { // TestEncryptionHandler_HelperFunctions tests the isAdmin and getActorFromGinContext helpers func TestEncryptionHandler_HelperFunctions(t *testing.T) { - gin.SetMode(gin.TestMode) t.Run("isAdmin with invalid role type", func(t *testing.T) { router := gin.New() @@ -787,7 +785,6 @@ func TestEncryptionHandler_RefreshKey_InvalidOldKey(t *testing.T) { // TestEncryptionHandler_GetActorFromGinContext_InvalidType tests getActorFromGinContext with invalid type func TestEncryptionHandler_GetActorFromGinContext_InvalidType(t *testing.T) { - gin.SetMode(gin.TestMode) router := gin.New() var capturedActor string @@ -884,7 +881,6 @@ func TestEncryptionHandler_RotateWithPartialFailures(t *testing.T) { // TestEncryptionHandler_isAdmin_NoRoleSet tests isAdmin when no role is set func TestEncryptionHandler_isAdmin_NoRoleSet(t *testing.T) { - gin.SetMode(gin.TestMode) router := gin.New() // No middleware setting user_role @@ -905,7 +901,6 @@ func TestEncryptionHandler_isAdmin_NoRoleSet(t *testing.T) { // TestEncryptionHandler_isAdmin_NonAdminRole tests isAdmin with non-admin role func TestEncryptionHandler_isAdmin_NonAdminRole(t *testing.T) { - gin.SetMode(gin.TestMode) router := gin.New() router.Use(func(c *gin.Context) { diff --git a/backend/internal/api/handlers/feature_flags_blocker3_test.go b/backend/internal/api/handlers/feature_flags_blocker3_test.go index 25cfe9ddb..050a03e92 100644 --- a/backend/internal/api/handlers/feature_flags_blocker3_test.go +++ b/backend/internal/api/handlers/feature_flags_blocker3_test.go @@ -15,7 +15,6 @@ import ( // TestBlocker3_SecurityProviderEventsFlagInResponse tests that the feature flag is included in GET response. func TestBlocker3_SecurityProviderEventsFlagInResponse(t *testing.T) { - gin.SetMode(gin.TestMode) // Setup test database db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) @@ -50,7 +49,6 @@ func TestBlocker3_SecurityProviderEventsFlagInResponse(t *testing.T) { // TestBlocker3_SecurityProviderEventsFlagDefaultValue tests the default value of the flag. func TestBlocker3_SecurityProviderEventsFlagDefaultValue(t *testing.T) { - gin.SetMode(gin.TestMode) // Setup test database db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) @@ -85,7 +83,6 @@ func TestBlocker3_SecurityProviderEventsFlagDefaultValue(t *testing.T) { // TestBlocker3_SecurityProviderEventsFlagCanBeEnabled tests that the flag can be enabled. func TestBlocker3_SecurityProviderEventsFlagCanBeEnabled(t *testing.T) { - gin.SetMode(gin.TestMode) // Setup test database db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) diff --git a/backend/internal/api/handlers/feature_flags_handler_coverage_test.go b/backend/internal/api/handlers/feature_flags_handler_coverage_test.go index dfe19cb95..83c18e794 100644 --- a/backend/internal/api/handlers/feature_flags_handler_coverage_test.go +++ b/backend/internal/api/handlers/feature_flags_handler_coverage_test.go @@ -15,7 +15,6 @@ import ( ) func TestFeatureFlagsHandler_GetFlags_DBPrecedence(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupFlagsDB(t) // Set a flag in DB @@ -48,7 +47,6 @@ func TestFeatureFlagsHandler_GetFlags_DBPrecedence(t *testing.T) { } func TestFeatureFlagsHandler_GetFlags_EnvFallback(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupFlagsDB(t) // Set env var (no DB value exists) @@ -73,7 +71,6 @@ func TestFeatureFlagsHandler_GetFlags_EnvFallback(t *testing.T) { } func TestFeatureFlagsHandler_GetFlags_EnvShortForm(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupFlagsDB(t) // Set short form env var (CERBERUS_ENABLED instead of FEATURE_CERBERUS_ENABLED) @@ -98,7 +95,6 @@ func TestFeatureFlagsHandler_GetFlags_EnvShortForm(t *testing.T) { } func TestFeatureFlagsHandler_GetFlags_EnvNumeric(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupFlagsDB(t) // Set numeric env var (1/0 instead of true/false) @@ -123,7 +119,6 @@ func TestFeatureFlagsHandler_GetFlags_EnvNumeric(t *testing.T) { } func TestFeatureFlagsHandler_GetFlags_DefaultTrue(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupFlagsDB(t) // No DB value, no env var - check defaults @@ -148,7 +143,6 @@ func TestFeatureFlagsHandler_GetFlags_DefaultTrue(t *testing.T) { } func TestFeatureFlagsHandler_GetFlags_AllDefaultFlagsPresent(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupFlagsDB(t) h := NewFeatureFlagsHandler(db) @@ -173,7 +167,6 @@ func TestFeatureFlagsHandler_GetFlags_AllDefaultFlagsPresent(t *testing.T) { } func TestFeatureFlagsHandler_UpdateFlags_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupFlagsDB(t) h := NewFeatureFlagsHandler(db) @@ -208,7 +201,6 @@ func TestFeatureFlagsHandler_UpdateFlags_Success(t *testing.T) { } func TestFeatureFlagsHandler_UpdateFlags_Upsert(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupFlagsDB(t) // Create existing setting @@ -249,7 +241,6 @@ func TestFeatureFlagsHandler_UpdateFlags_Upsert(t *testing.T) { } func TestFeatureFlagsHandler_UpdateFlags_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupFlagsDB(t) h := NewFeatureFlagsHandler(db) @@ -265,7 +256,6 @@ func TestFeatureFlagsHandler_UpdateFlags_InvalidJSON(t *testing.T) { } func TestFeatureFlagsHandler_UpdateFlags_OnlyAllowedKeys(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupFlagsDB(t) h := NewFeatureFlagsHandler(db) @@ -298,7 +288,6 @@ func TestFeatureFlagsHandler_UpdateFlags_OnlyAllowedKeys(t *testing.T) { } func TestFeatureFlagsHandler_UpdateFlags_EmptyPayload(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupFlagsDB(t) h := NewFeatureFlagsHandler(db) @@ -339,7 +328,6 @@ func TestFeatureFlagsHandler_GetFlags_DBValueVariants(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupFlagsDB(t) // Set flag with test value @@ -387,7 +375,6 @@ func TestFeatureFlagsHandler_GetFlags_EnvValueVariants(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupFlagsDB(t) // Set env var (no DB value) @@ -425,7 +412,6 @@ func TestFeatureFlagsHandler_UpdateFlags_BoolValues(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupFlagsDB(t) h := NewFeatureFlagsHandler(db) @@ -462,7 +448,6 @@ func TestFeatureFlagsHandler_NewFeatureFlagsHandler(t *testing.T) { } func TestFeatureFlagsHandler_GetFlags_EmailFlagDefaultFalse(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupFlagsDB(t) h := NewFeatureFlagsHandler(db) diff --git a/backend/internal/api/handlers/feature_flags_handler_test.go b/backend/internal/api/handlers/feature_flags_handler_test.go index 908814518..1da8b7687 100644 --- a/backend/internal/api/handlers/feature_flags_handler_test.go +++ b/backend/internal/api/handlers/feature_flags_handler_test.go @@ -28,7 +28,6 @@ func TestFeatureFlags_GetAndUpdate(t *testing.T) { h := NewFeatureFlagsHandler(db) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/api/v1/feature-flags", h.GetFlags) r.PUT("/api/v1/feature-flags", h.UpdateFlags) @@ -81,7 +80,6 @@ func TestFeatureFlags_EnvFallback(t *testing.T) { db := setupFlagsDB(t) // Do not write any settings so DB lookup fails and env is used h := NewFeatureFlagsHandler(db) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/api/v1/feature-flags", h.GetFlags) @@ -178,7 +176,6 @@ func TestGetFlags_BatchQuery(t *testing.T) { db.Create(&models.Setting{Key: "feature.crowdsec.console_enrollment", Value: "true", Type: "bool", Category: "feature"}) h := NewFeatureFlagsHandler(db) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/api/v1/feature-flags", h.GetFlags) @@ -219,7 +216,6 @@ func TestUpdateFlags_TransactionRollback(t *testing.T) { _ = sqlDB.Close() h := NewFeatureFlagsHandler(db) - gin.SetMode(gin.TestMode) r := gin.New() r.PUT("/api/v1/feature-flags", h.UpdateFlags) @@ -244,7 +240,6 @@ func TestUpdateFlags_TransactionAtomic(t *testing.T) { db := setupFlagsDB(t) h := NewFeatureFlagsHandler(db) - gin.SetMode(gin.TestMode) r := gin.New() r.PUT("/api/v1/feature-flags", h.UpdateFlags) diff --git a/backend/internal/api/handlers/handlers_blackbox_test.go b/backend/internal/api/handlers/handlers_blackbox_test.go index 1ecaeacd8..acc686b1b 100644 --- a/backend/internal/api/handlers/handlers_blackbox_test.go +++ b/backend/internal/api/handlers/handlers_blackbox_test.go @@ -50,7 +50,6 @@ func addAdminMiddleware(router *gin.Engine) { } func TestImportHandler_GetStatus(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) // Case 1: No active session, no mount @@ -78,7 +77,6 @@ func TestImportHandler_GetStatus(t *testing.T) { } func TestImportHandler_Commit(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) handler := handlers.NewImportHandler(db, "echo", "/tmp", "") router := gin.New() @@ -120,7 +118,6 @@ func TestImportHandler_Commit(t *testing.T) { } func TestImportHandler_Upload(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) // Use fake caddy script @@ -151,7 +148,6 @@ func TestImportHandler_Upload(t *testing.T) { } func TestImportHandler_GetPreview_WithContent(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) tmpDir := t.TempDir() handler := handlers.NewImportHandler(db, "echo", tmpDir, "") @@ -188,7 +184,6 @@ func TestImportHandler_GetPreview_WithContent(t *testing.T) { } func TestImportHandler_Commit_Errors(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) handler := handlers.NewImportHandler(db, "echo", "/tmp", "") router := gin.New() @@ -233,7 +228,6 @@ func TestImportHandler_Commit_Errors(t *testing.T) { } func TestImportHandler_Cancel_Errors(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) handler := handlers.NewImportHandler(db, "echo", "/tmp", "") router := gin.New() @@ -279,7 +273,6 @@ func TestCheckMountedImport(t *testing.T) { } func TestImportHandler_Upload_Failure(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) // Use fake caddy script that fails @@ -310,7 +303,6 @@ func TestImportHandler_Upload_Failure(t *testing.T) { } func TestImportHandler_Upload_Conflict(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) // Pre-create a host to cause conflict @@ -359,7 +351,6 @@ func TestImportHandler_Upload_Conflict(t *testing.T) { } func TestImportHandler_GetPreview_BackupContent(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) tmpDir := t.TempDir() handler := handlers.NewImportHandler(db, "echo", tmpDir, "") @@ -410,7 +401,6 @@ func TestImportHandler_RegisterRoutes(t *testing.T) { } func TestImportHandler_GetPreview_TransientMount(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) tmpDir := t.TempDir() mountPath := filepath.Join(tmpDir, "mounted.caddyfile") @@ -455,7 +445,6 @@ func TestImportHandler_GetPreview_TransientMount(t *testing.T) { } func TestImportHandler_Commit_TransientUpload(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) tmpDir := t.TempDir() @@ -515,7 +504,6 @@ func TestImportHandler_Commit_TransientUpload(t *testing.T) { } func TestImportHandler_Commit_TransientMount(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) tmpDir := t.TempDir() mountPath := filepath.Join(tmpDir, "mounted.caddyfile") @@ -562,7 +550,6 @@ func TestImportHandler_Commit_TransientMount(t *testing.T) { } func TestImportHandler_Cancel_TransientUpload(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) tmpDir := t.TempDir() @@ -597,7 +584,6 @@ func TestImportHandler_Cancel_TransientUpload(t *testing.T) { } func TestImportHandler_DetectImports(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) handler := handlers.NewImportHandler(db, "echo", "/tmp", "") router := gin.New() @@ -660,7 +646,6 @@ func TestImportHandler_DetectImports(t *testing.T) { } func TestImportHandler_DetectImports_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) handler := handlers.NewImportHandler(db, "echo", "/tmp", "") router := gin.New() @@ -676,7 +661,6 @@ func TestImportHandler_DetectImports_InvalidJSON(t *testing.T) { } func TestImportHandler_UploadMulti(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) tmpDir := t.TempDir() @@ -791,7 +775,6 @@ func TestImportHandler_UploadMulti(t *testing.T) { // Additional tests for comprehensive coverage func TestImportHandler_Cancel_MissingSessionUUID(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) handler := handlers.NewImportHandler(db, "echo", "/tmp", "") router := gin.New() @@ -810,7 +793,6 @@ func TestImportHandler_Cancel_MissingSessionUUID(t *testing.T) { } func TestImportHandler_Cancel_InvalidSessionUUID(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) handler := handlers.NewImportHandler(db, "echo", "/tmp", "") router := gin.New() @@ -829,7 +811,6 @@ func TestImportHandler_Cancel_InvalidSessionUUID(t *testing.T) { } func TestImportHandler_Commit_InvalidSessionUUID(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) handler := handlers.NewImportHandler(db, "echo", "/tmp", "") router := gin.New() @@ -884,7 +865,6 @@ func (m *mockProxyHostService) List() ([]models.ProxyHost, error) { // TestImportHandler_Commit_UpdateFailure tests the error logging path when Update fails (line 676) func TestImportHandler_Commit_UpdateFailure(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) // Create an existing host that we'll try to overwrite @@ -959,7 +939,6 @@ func TestImportHandler_Commit_UpdateFailure(t *testing.T) { // TestImportHandler_Commit_CreateFailure tests the error logging path when Create fails (line 682) func TestImportHandler_Commit_CreateFailure(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) // Create an existing host to cause a duplicate error @@ -1019,7 +998,6 @@ func TestImportHandler_Commit_CreateFailure(t *testing.T) { // TestUpload_NormalizationSuccess tests the success path where NormalizeCaddyfile succeeds (line 271) func TestUpload_NormalizationSuccess(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) // Use fake caddy script that handles both fmt and adapt @@ -1065,7 +1043,6 @@ func TestUpload_NormalizationSuccess(t *testing.T) { // TestUpload_NormalizationFallback tests the fallback path where NormalizeCaddyfile fails (line 269) func TestUpload_NormalizationFallback(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) // Use fake caddy script that fails fmt but succeeds on adapt @@ -1113,7 +1090,6 @@ func TestUpload_NormalizationFallback(t *testing.T) { // TestCommit_OverwriteAction tests that overwrite preserves certificate ID func TestCommit_OverwriteAction(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) // Create existing host with certificate association @@ -1184,7 +1160,6 @@ func ptrToUint(v uint) *uint { // TestCommit_RenameAction tests that rename appends suffix func TestCommit_RenameAction(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) // Create existing host @@ -1252,7 +1227,6 @@ func TestCommit_RenameAction(t *testing.T) { } func TestGetPreview_WithConflictDetails(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) tmpDir := t.TempDir() mountPath := filepath.Join(tmpDir, "mounted.caddyfile") @@ -1310,7 +1284,6 @@ func TestGetPreview_WithConflictDetails(t *testing.T) { } func TestSafeJoin_PathTraversalCases(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) tmpDir := t.TempDir() handler := handlers.NewImportHandler(db, "echo", tmpDir, "") @@ -1375,7 +1348,6 @@ func TestSafeJoin_PathTraversalCases(t *testing.T) { } func TestCommit_SkipAction(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) session := models.ImportSession{ @@ -1433,7 +1405,6 @@ func TestCommit_SkipAction(t *testing.T) { } func TestCommit_CustomNames(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) session := models.ImportSession{ @@ -1483,7 +1454,6 @@ func TestCommit_CustomNames(t *testing.T) { } func TestGetStatus_AlreadyCommittedMount(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) tmpDir := t.TempDir() mountPath := filepath.Join(tmpDir, "mounted.caddyfile") @@ -1519,7 +1489,6 @@ func TestGetStatus_AlreadyCommittedMount(t *testing.T) { } func TestImportHandler_Commit_SessionSaveWarning(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) // Create an import session with one host to create @@ -1591,7 +1560,6 @@ func newTestImportHandler(t *testing.T, db *gorm.DB, importDir string, mountPath // TestGetStatus_DatabaseError tests GetStatus when database query fails func TestGetStatus_DatabaseError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) handler := newTestImportHandler(t, db, t.TempDir(), "") @@ -1613,7 +1581,6 @@ func TestGetStatus_DatabaseError(t *testing.T) { // TestGetPreview_MountAlreadyCommitted tests GetPreview when mount is already committed with FUTURE timestamp func TestGetPreview_MountAlreadyCommitted(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) // Create mount file @@ -1648,7 +1615,6 @@ func TestGetPreview_MountAlreadyCommitted(t *testing.T) { // TestUpload_MkdirAllFailure tests Upload when MkdirAll fails func TestUpload_MkdirAllFailure(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportTestDB(t) // Create a FILE where uploads directory should be (blocks MkdirAll) diff --git a/backend/internal/api/handlers/handlers_test.go b/backend/internal/api/handlers/handlers_test.go index 996234a18..2f71217fb 100644 --- a/backend/internal/api/handlers/handlers_test.go +++ b/backend/internal/api/handlers/handlers_test.go @@ -36,7 +36,6 @@ func setupTestDB(t *testing.T) *gorm.DB { func TestRemoteServerHandler_List(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupTestDB(t) // Create test server @@ -71,7 +70,6 @@ func TestRemoteServerHandler_List(t *testing.T) { func TestRemoteServerHandler_Create(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupTestDB(t) ns := services.NewNotificationService(db, nil) @@ -105,7 +103,6 @@ func TestRemoteServerHandler_Create(t *testing.T) { func TestRemoteServerHandler_TestConnection(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupTestDB(t) // Create test server @@ -140,7 +137,6 @@ func TestRemoteServerHandler_TestConnection(t *testing.T) { func TestRemoteServerHandler_Get(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupTestDB(t) // Create test server @@ -174,7 +170,6 @@ func TestRemoteServerHandler_Get(t *testing.T) { func TestRemoteServerHandler_Update(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupTestDB(t) // Create test server @@ -220,7 +215,6 @@ func TestRemoteServerHandler_Update(t *testing.T) { func TestRemoteServerHandler_Delete(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupTestDB(t) // Create test server @@ -256,7 +250,6 @@ func TestRemoteServerHandler_Delete(t *testing.T) { func TestProxyHostHandler_List(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupTestDB(t) // Create test proxy host @@ -292,7 +285,6 @@ func TestProxyHostHandler_List(t *testing.T) { func TestProxyHostHandler_Create(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupTestDB(t) ns := services.NewNotificationService(db, nil) @@ -328,7 +320,6 @@ func TestProxyHostHandler_Create(t *testing.T) { func TestProxyHostHandler_PartialUpdate_DoesNotWipeFields(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupTestDB(t) // Seed a proxy host @@ -386,7 +377,6 @@ func TestProxyHostHandler_PartialUpdate_DoesNotWipeFields(t *testing.T) { func TestHealthHandler(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) router := gin.New() router.GET("/health", handlers.HealthHandler) @@ -405,7 +395,6 @@ func TestHealthHandler(t *testing.T) { func TestRemoteServerHandler_Errors(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupTestDB(t) ns := services.NewNotificationService(db, nil) diff --git a/backend/internal/api/handlers/health_handler_test.go b/backend/internal/api/handlers/health_handler_test.go index 2ed9e5f02..11ec8f4b9 100644 --- a/backend/internal/api/handlers/health_handler_test.go +++ b/backend/internal/api/handlers/health_handler_test.go @@ -11,7 +11,6 @@ import ( ) func TestHealthHandler(t *testing.T) { - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/health", HealthHandler) diff --git a/backend/internal/api/handlers/import_handler_coverage_test.go b/backend/internal/api/handlers/import_handler_coverage_test.go index 418d487da..a6cc97873 100644 --- a/backend/internal/api/handlers/import_handler_coverage_test.go +++ b/backend/internal/api/handlers/import_handler_coverage_test.go @@ -101,7 +101,6 @@ func (m *MockImporterService) ValidateCaddyBinary() error { // TestUploadMulti_EmptyList covers the manual check for len(Files) == 0 func TestUploadMulti_EmptyList(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportCoverageTestDB(t) @@ -135,7 +134,6 @@ func TestUploadMulti_EmptyList(t *testing.T) { // TestUploadMulti_FileServerDetected covers the logic where parsable routes trigger a warning // because they contain file_server but no valid reverse_proxy hosts func TestUploadMulti_FileServerDetected(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportCoverageTestDB(t) mockSvc := new(MockImporterService) @@ -185,7 +183,6 @@ func TestUploadMulti_FileServerDetected(t *testing.T) { // TestUploadMulti_NoSitesParsed covers successfull parsing but 0 result hosts func TestUploadMulti_NoSitesParsed(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportCoverageTestDB(t) mockSvc := new(MockImporterService) @@ -227,7 +224,6 @@ func TestUploadMulti_NoSitesParsed(t *testing.T) { } func TestUpload_ImportsDetectedNoImportableHosts(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportCoverageTestDB(t) mockSvc := new(MockImporterService) @@ -263,7 +259,6 @@ func TestUpload_ImportsDetectedNoImportableHosts(t *testing.T) { } func TestUploadMulti_RequiresMainCaddyfile(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportCoverageTestDB(t) h := NewImportHandler(db, "caddy", t.TempDir(), "") @@ -291,7 +286,6 @@ func TestUploadMulti_RequiresMainCaddyfile(t *testing.T) { } func TestUploadMulti_RejectsEmptyFileContent(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportCoverageTestDB(t) h := NewImportHandler(db, "caddy", t.TempDir(), "") @@ -319,7 +313,6 @@ func TestUploadMulti_RejectsEmptyFileContent(t *testing.T) { } func TestCommitAndCancel_InvalidSessionUUID(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportCoverageTestDB(t) tmpImport := t.TempDir() @@ -352,7 +345,6 @@ func TestCommitAndCancel_InvalidSessionUUID(t *testing.T) { } func TestCancel_RemovesTransientUpload(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupImportCoverageTestDB(t) tmpImport := t.TempDir() @@ -381,7 +373,6 @@ func TestCancel_RemovesTransientUpload(t *testing.T) { } func TestUpload_ReadOnlyDBRespondsWithPermissionError(t *testing.T) { - gin.SetMode(gin.TestMode) roDB := setupReadOnlyImportDB(t) mockSvc := new(MockImporterService) @@ -414,7 +405,6 @@ func TestUpload_ReadOnlyDBRespondsWithPermissionError(t *testing.T) { } func TestUploadMulti_ReadOnlyDBRespondsWithPermissionError(t *testing.T) { - gin.SetMode(gin.TestMode) roDB := setupReadOnlyImportDB(t) mockSvc := new(MockImporterService) @@ -448,7 +438,6 @@ func TestUploadMulti_ReadOnlyDBRespondsWithPermissionError(t *testing.T) { } func TestCommit_ReadOnlyDBSaveRespondsWithPermissionError(t *testing.T) { - gin.SetMode(gin.TestMode) roDB := setupReadOnlyImportDB(t) mockSvc := new(MockImporterService) @@ -483,7 +472,6 @@ func TestCommit_ReadOnlyDBSaveRespondsWithPermissionError(t *testing.T) { } func TestCancel_ReadOnlyDBSaveRespondsWithPermissionError(t *testing.T) { - gin.SetMode(gin.TestMode) tmp := t.TempDir() dbPath := filepath.Join(tmp, "cancel_ro.db") diff --git a/backend/internal/api/handlers/import_handler_sanitize_test.go b/backend/internal/api/handlers/import_handler_sanitize_test.go index 8609f0290..f9452ccfd 100644 --- a/backend/internal/api/handlers/import_handler_sanitize_test.go +++ b/backend/internal/api/handlers/import_handler_sanitize_test.go @@ -17,7 +17,6 @@ import ( ) func TestImportUploadSanitizesFilename(t *testing.T) { - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() // set up in-memory DB for handler db := OpenTestDB(t) diff --git a/backend/internal/api/handlers/import_handler_test.go b/backend/internal/api/handlers/import_handler_test.go index 3e8b5050e..52d7c3180 100644 --- a/backend/internal/api/handlers/import_handler_test.go +++ b/backend/internal/api/handlers/import_handler_test.go @@ -136,7 +136,6 @@ func TestImportHandler_GetStatus_MountCommittedUnchanged(t *testing.T) { handler, _, _ := setupTestHandler(t, tx) handler.mountPath = mountPath - gin.SetMode(gin.TestMode) router := gin.New() addAdminMiddleware(router) handler.RegisterRoutes(router.Group("/api/v1")) @@ -173,7 +172,6 @@ func TestImportHandler_GetStatus_MountModifiedAfterCommit(t *testing.T) { handler, _, _ := setupTestHandler(t, tx) handler.mountPath = mountPath - gin.SetMode(gin.TestMode) router := gin.New() addAdminMiddleware(router) handler.RegisterRoutes(router.Group("/api/v1")) @@ -223,7 +221,6 @@ func TestUpload_NormalizationSuccess(t *testing.T) { req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - gin.SetMode(gin.TestMode) router := gin.New() addAdminMiddleware(router) handler.RegisterRoutes(router.Group("/api/v1")) @@ -272,7 +269,6 @@ func TestUpload_NormalizationFailure(t *testing.T) { req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - gin.SetMode(gin.TestMode) router := gin.New() addAdminMiddleware(router) handler.RegisterRoutes(router.Group("/api/v1")) @@ -313,7 +309,6 @@ func TestUpload_PathTraversalBlocked(t *testing.T) { req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - gin.SetMode(gin.TestMode) router := gin.New() addAdminMiddleware(router) handler.RegisterRoutes(router.Group("/api/v1")) @@ -354,7 +349,6 @@ func TestUploadMulti_ArchiveExtraction(t *testing.T) { req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - gin.SetMode(gin.TestMode) router := gin.New() addAdminMiddleware(router) handler.RegisterRoutes(router.Group("/api/v1")) @@ -400,7 +394,6 @@ func TestUploadMulti_ConflictDetection(t *testing.T) { req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - gin.SetMode(gin.TestMode) router := gin.New() addAdminMiddleware(router) handler.RegisterRoutes(router.Group("/api/v1")) @@ -439,7 +432,6 @@ func TestCommit_TransientToImport(t *testing.T) { req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - gin.SetMode(gin.TestMode) router := gin.New() addAdminMiddleware(router) handler.RegisterRoutes(router.Group("/api/v1")) @@ -484,7 +476,6 @@ func TestCommit_RollbackOnError(t *testing.T) { req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - gin.SetMode(gin.TestMode) router := gin.New() addAdminMiddleware(router) handler.RegisterRoutes(router.Group("/api/v1")) @@ -517,7 +508,6 @@ func TestDetectImports_EmptyCaddyfile(t *testing.T) { req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - gin.SetMode(gin.TestMode) router := gin.New() addAdminMiddleware(router) handler.RegisterRoutes(router.Group("/api/v1")) @@ -662,7 +652,6 @@ func TestImportHandler_Upload_NullByteInjection(t *testing.T) { req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - gin.SetMode(gin.TestMode) router := gin.New() addAdminMiddleware(router) handler.RegisterRoutes(router.Group("/api/v1")) @@ -689,7 +678,6 @@ func TestImportHandler_DetectImports_MalformedFile(t *testing.T) { req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - gin.SetMode(gin.TestMode) router := gin.New() addAdminMiddleware(router) handler.RegisterRoutes(router.Group("/api/v1")) @@ -835,7 +823,6 @@ func TestImportHandler_Upload_InvalidSessionPaths(t *testing.T) { req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - gin.SetMode(gin.TestMode) router := gin.New() addAdminMiddleware(router) handler.RegisterRoutes(router.Group("/api/v1")) @@ -860,7 +847,6 @@ func TestImportHandler_Commit_InvalidSessionUUID_BranchCoverage(t *testing.T) { req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - gin.SetMode(gin.TestMode) router := gin.New() addAdminMiddleware(router) handler.RegisterRoutes(router.Group("/api/v1")) @@ -893,7 +879,6 @@ func TestImportHandler_Upload_NoImportableHosts_WithImportsDetected(t *testing.T req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - gin.SetMode(gin.TestMode) router := gin.New() addAdminMiddleware(router) handler.RegisterRoutes(router.Group("/api/v1")) @@ -925,7 +910,6 @@ func TestImportHandler_Upload_NoImportableHosts_NoImportsNoFileServer(t *testing req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - gin.SetMode(gin.TestMode) router := gin.New() addAdminMiddleware(router) handler.RegisterRoutes(router.Group("/api/v1")) @@ -967,7 +951,6 @@ func TestImportHandler_Commit_OverwriteAndRenameFlows(t *testing.T) { req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - gin.SetMode(gin.TestMode) router := gin.New() addAdminMiddleware(router) handler.RegisterRoutes(router.Group("/api/v1")) @@ -986,7 +969,6 @@ func TestImportHandler_Cancel_ValidationAndNotFound_BranchCoverage(t *testing.T) testutil.WithTx(t, setupImportTestDB(t), func(tx *gorm.DB) { handler, _, _ := setupTestHandler(t, tx) - gin.SetMode(gin.TestMode) router := gin.New() addAdminMiddleware(router) handler.RegisterRoutes(router.Group("/api/v1")) @@ -1021,7 +1003,6 @@ func TestImportHandler_Cancel_TransientUploadCancelled_BranchCoverage(t *testing uploadPath := filepath.Join(uploadDir, sessionID+".caddyfile") require.NoError(t, os.WriteFile(uploadPath, []byte("example.com { respond \"ok\" }"), 0o600)) - gin.SetMode(gin.TestMode) router := gin.New() addAdminMiddleware(router) handler.RegisterRoutes(router.Group("/api/v1")) diff --git a/backend/internal/api/handlers/json_import_handler_test.go b/backend/internal/api/handlers/json_import_handler_test.go index 3345dd2a6..409ac5f29 100644 --- a/backend/internal/api/handlers/json_import_handler_test.go +++ b/backend/internal/api/handlers/json_import_handler_test.go @@ -40,7 +40,6 @@ func TestJSONImportHandler_RegisterRoutes(t *testing.T) { db := setupJSONTestDB(t) handler := NewJSONImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) @@ -60,7 +59,6 @@ func TestJSONImportHandler_Upload_CharonFormat(t *testing.T) { db := setupJSONTestDB(t) handler := NewJSONImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) @@ -119,7 +117,6 @@ func TestJSONImportHandler_Upload_NPMFormatFallback(t *testing.T) { db := setupJSONTestDB(t) handler := NewJSONImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) @@ -162,7 +159,6 @@ func TestJSONImportHandler_Upload_UnrecognizedFormat(t *testing.T) { db := setupJSONTestDB(t) handler := NewJSONImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) @@ -188,7 +184,6 @@ func TestJSONImportHandler_Upload_InvalidJSON(t *testing.T) { db := setupJSONTestDB(t) handler := NewJSONImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) @@ -208,7 +203,6 @@ func TestJSONImportHandler_Commit_CharonFormat(t *testing.T) { db := setupJSONTestDB(t) handler := NewJSONImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) @@ -277,7 +271,6 @@ func TestJSONImportHandler_Commit_NPMFormatFallback(t *testing.T) { db := setupJSONTestDB(t) handler := NewJSONImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) @@ -339,7 +332,6 @@ func TestJSONImportHandler_Commit_SessionNotFound(t *testing.T) { db := setupJSONTestDB(t) handler := NewJSONImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) @@ -370,7 +362,6 @@ func TestJSONImportHandler_Cancel(t *testing.T) { db := setupJSONTestDB(t) handler := NewJSONImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) @@ -459,7 +450,6 @@ func TestJSONImportHandler_ConflictDetection(t *testing.T) { handler := NewJSONImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) @@ -501,7 +491,6 @@ func TestJSONImportHandler_Cancel_RequiresValidJSONBody(t *testing.T) { db := setupJSONTestDB(t) handler := NewJSONImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) diff --git a/backend/internal/api/handlers/logs_handler_coverage_test.go b/backend/internal/api/handlers/logs_handler_coverage_test.go index e09edea2b..1192cd1bc 100644 --- a/backend/internal/api/handlers/logs_handler_coverage_test.go +++ b/backend/internal/api/handlers/logs_handler_coverage_test.go @@ -17,7 +17,6 @@ import ( ) func TestLogsHandler_Read_FilterBySearch(t *testing.T) { - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") @@ -50,7 +49,6 @@ func TestLogsHandler_Read_FilterBySearch(t *testing.T) { } func TestLogsHandler_Read_FilterByHost(t *testing.T) { - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") @@ -80,7 +78,6 @@ func TestLogsHandler_Read_FilterByHost(t *testing.T) { } func TestLogsHandler_Read_FilterByLevel(t *testing.T) { - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") @@ -110,7 +107,6 @@ func TestLogsHandler_Read_FilterByLevel(t *testing.T) { } func TestLogsHandler_Read_FilterByStatus(t *testing.T) { - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") @@ -140,7 +136,6 @@ func TestLogsHandler_Read_FilterByStatus(t *testing.T) { } func TestLogsHandler_Read_SortAsc(t *testing.T) { - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") @@ -170,7 +165,6 @@ func TestLogsHandler_Read_SortAsc(t *testing.T) { } func TestLogsHandler_List_DirectoryIsFile(t *testing.T) { - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") @@ -197,7 +191,6 @@ func TestLogsHandler_List_DirectoryIsFile(t *testing.T) { } func TestLogsHandler_Download_TempFileError(t *testing.T) { - gin.SetMode(gin.TestMode) tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") diff --git a/backend/internal/api/handlers/logs_ws_test.go b/backend/internal/api/handlers/logs_ws_test.go index 06034712b..d477bff68 100644 --- a/backend/internal/api/handlers/logs_ws_test.go +++ b/backend/internal/api/handlers/logs_ws_test.go @@ -71,7 +71,6 @@ func TestUpgraderCheckOrigin(t *testing.T) { } func TestLogsWebSocketHandler_DeprecatedWrapperUpgradeFailure(t *testing.T) { - gin.SetMode(gin.TestMode) charonlogger.Init(false, io.Discard) r := gin.New() @@ -85,7 +84,6 @@ func TestLogsWebSocketHandler_DeprecatedWrapperUpgradeFailure(t *testing.T) { } func TestLogsWSHandler_StreamWithFiltersAndTracker(t *testing.T) { - gin.SetMode(gin.TestMode) charonlogger.Init(false, io.Discard) tracker := services.NewWebSocketTracker() diff --git a/backend/internal/api/handlers/manual_challenge_handler_test.go b/backend/internal/api/handlers/manual_challenge_handler_test.go index d03503f15..8a3d18a2b 100644 --- a/backend/internal/api/handlers/manual_challenge_handler_test.go +++ b/backend/internal/api/handlers/manual_challenge_handler_test.go @@ -82,7 +82,6 @@ func (m *mockDNSProviderServiceForChallenge) Get(ctx context.Context, id uint) ( } func setupChallengeTestRouter() *gin.Engine { - gin.SetMode(gin.TestMode) return gin.New() } @@ -507,7 +506,6 @@ func TestGetUserIDFromContext(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gin.SetMode(gin.TestMode) c, _ := gin.CreateTestContext(httptest.NewRecorder()) if tt.value != nil { c.Set("user_id", tt.value) diff --git a/backend/internal/api/handlers/misc_coverage_test.go b/backend/internal/api/handlers/misc_coverage_test.go index 0b2a59398..3f836efec 100644 --- a/backend/internal/api/handlers/misc_coverage_test.go +++ b/backend/internal/api/handlers/misc_coverage_test.go @@ -23,7 +23,6 @@ func setupDomainCoverageDB(t *testing.T) *gorm.DB { } func TestDomainHandler_List_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupDomainCoverageDB(t) h := NewDomainHandler(db, nil) @@ -40,7 +39,6 @@ func TestDomainHandler_List_Error(t *testing.T) { } func TestDomainHandler_Create_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupDomainCoverageDB(t) h := NewDomainHandler(db, nil) @@ -55,7 +53,6 @@ func TestDomainHandler_Create_InvalidJSON(t *testing.T) { } func TestDomainHandler_Create_DBError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupDomainCoverageDB(t) h := NewDomainHandler(db, nil) @@ -76,7 +73,6 @@ func TestDomainHandler_Create_DBError(t *testing.T) { } func TestDomainHandler_Delete_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupDomainCoverageDB(t) h := NewDomainHandler(db, nil) @@ -103,7 +99,6 @@ func setupRemoteServerCoverageDB(t *testing.T) *gorm.DB { } func TestRemoteServerHandler_List_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupRemoteServerCoverageDB(t) svc := services.NewRemoteServerService(db) h := NewRemoteServerHandler(svc, nil) @@ -121,7 +116,6 @@ func TestRemoteServerHandler_List_Error(t *testing.T) { } func TestRemoteServerHandler_List_EnabledOnly(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupRemoteServerCoverageDB(t) svc := services.NewRemoteServerService(db) h := NewRemoteServerHandler(svc, nil) @@ -140,7 +134,6 @@ func TestRemoteServerHandler_List_EnabledOnly(t *testing.T) { } func TestRemoteServerHandler_Update_NotFound(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupRemoteServerCoverageDB(t) svc := services.NewRemoteServerService(db) h := NewRemoteServerHandler(svc, nil) @@ -155,7 +148,6 @@ func TestRemoteServerHandler_Update_NotFound(t *testing.T) { } func TestRemoteServerHandler_Update_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupRemoteServerCoverageDB(t) svc := services.NewRemoteServerService(db) h := NewRemoteServerHandler(svc, nil) @@ -176,7 +168,6 @@ func TestRemoteServerHandler_Update_InvalidJSON(t *testing.T) { } func TestRemoteServerHandler_TestConnection_NotFound(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupRemoteServerCoverageDB(t) svc := services.NewRemoteServerService(db) h := NewRemoteServerHandler(svc, nil) @@ -191,7 +182,6 @@ func TestRemoteServerHandler_TestConnection_NotFound(t *testing.T) { } func TestRemoteServerHandler_TestConnectionCustom_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupRemoteServerCoverageDB(t) svc := services.NewRemoteServerService(db) h := NewRemoteServerHandler(svc, nil) @@ -207,7 +197,6 @@ func TestRemoteServerHandler_TestConnectionCustom_InvalidJSON(t *testing.T) { } func TestRemoteServerHandler_TestConnectionCustom_Unreachable(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupRemoteServerCoverageDB(t) svc := services.NewRemoteServerService(db) h := NewRemoteServerHandler(svc, nil) @@ -239,7 +228,6 @@ func setupUptimeCoverageDB(t *testing.T) *gorm.DB { } func TestUptimeHandler_List_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUptimeCoverageDB(t) svc := services.NewUptimeService(db, nil) h := NewUptimeHandler(svc) @@ -257,7 +245,6 @@ func TestUptimeHandler_List_Error(t *testing.T) { } func TestUptimeHandler_GetHistory_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUptimeCoverageDB(t) svc := services.NewUptimeService(db, nil) h := NewUptimeHandler(svc) @@ -276,7 +263,6 @@ func TestUptimeHandler_GetHistory_Error(t *testing.T) { } func TestUptimeHandler_Update_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUptimeCoverageDB(t) svc := services.NewUptimeService(db, nil) h := NewUptimeHandler(svc) @@ -293,7 +279,6 @@ func TestUptimeHandler_Update_InvalidJSON(t *testing.T) { } func TestUptimeHandler_Sync_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUptimeCoverageDB(t) svc := services.NewUptimeService(db, nil) h := NewUptimeHandler(svc) @@ -311,7 +296,6 @@ func TestUptimeHandler_Sync_Error(t *testing.T) { } func TestUptimeHandler_Delete_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUptimeCoverageDB(t) svc := services.NewUptimeService(db, nil) h := NewUptimeHandler(svc) @@ -330,7 +314,6 @@ func TestUptimeHandler_Delete_Error(t *testing.T) { } func TestUptimeHandler_CheckMonitor_NotFound(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUptimeCoverageDB(t) svc := services.NewUptimeService(db, nil) h := NewUptimeHandler(svc) diff --git a/backend/internal/api/handlers/notification_coverage_test.go b/backend/internal/api/handlers/notification_coverage_test.go index 7ddc0c287..7f2b5156a 100644 --- a/backend/internal/api/handlers/notification_coverage_test.go +++ b/backend/internal/api/handlers/notification_coverage_test.go @@ -33,7 +33,6 @@ func setAdminContext(c *gin.Context) { // Notification Handler Tests func TestNotificationHandler_List_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationHandler(svc) @@ -55,7 +54,6 @@ func TestNotificationHandler_List_Error(t *testing.T) { } func TestNotificationHandler_List_UnreadOnly(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationHandler(svc) @@ -75,7 +73,6 @@ func TestNotificationHandler_List_UnreadOnly(t *testing.T) { } func TestNotificationHandler_MarkAsRead_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationHandler(svc) @@ -95,7 +92,6 @@ func TestNotificationHandler_MarkAsRead_Error(t *testing.T) { } func TestNotificationHandler_MarkAllAsRead_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationHandler(svc) @@ -116,7 +112,6 @@ func TestNotificationHandler_MarkAllAsRead_Error(t *testing.T) { // Notification Provider Handler Tests func TestNotificationProviderHandler_List_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -135,7 +130,6 @@ func TestNotificationProviderHandler_List_Error(t *testing.T) { } func TestNotificationProviderHandler_Create_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -152,7 +146,6 @@ func TestNotificationProviderHandler_Create_InvalidJSON(t *testing.T) { } func TestNotificationProviderHandler_Create_DBError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -180,7 +173,6 @@ func TestNotificationProviderHandler_Create_DBError(t *testing.T) { } func TestNotificationProviderHandler_Create_InvalidTemplate(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -206,7 +198,6 @@ func TestNotificationProviderHandler_Create_InvalidTemplate(t *testing.T) { } func TestNotificationProviderHandler_Update_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -224,7 +215,6 @@ func TestNotificationProviderHandler_Update_InvalidJSON(t *testing.T) { } func TestNotificationProviderHandler_Update_InvalidTemplate(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -256,7 +246,6 @@ func TestNotificationProviderHandler_Update_InvalidTemplate(t *testing.T) { } func TestNotificationProviderHandler_Update_DBError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -285,7 +274,6 @@ func TestNotificationProviderHandler_Update_DBError(t *testing.T) { } func TestNotificationProviderHandler_Delete_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -305,7 +293,6 @@ func TestNotificationProviderHandler_Delete_Error(t *testing.T) { } func TestNotificationProviderHandler_Test_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -322,7 +309,6 @@ func TestNotificationProviderHandler_Test_InvalidJSON(t *testing.T) { } func TestNotificationProviderHandler_Test_RejectsClientSuppliedGotifyToken(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -354,7 +340,6 @@ func TestNotificationProviderHandler_Test_RejectsClientSuppliedGotifyToken(t *te } func TestNotificationProviderHandler_Test_RejectsGotifyTokenWithWhitespace(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -499,7 +484,6 @@ func TestClassifyProviderTestFailure_SlackNoService(t *testing.T) { } func TestNotificationProviderHandler_Test_RejectsSlackTokenInTestRequest(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -530,7 +514,6 @@ func TestNotificationProviderHandler_Test_RejectsSlackTokenInTestRequest(t *test } func TestNotificationProviderHandler_Templates(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -548,7 +531,6 @@ func TestNotificationProviderHandler_Templates(t *testing.T) { } func TestNotificationProviderHandler_Preview_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -565,7 +547,6 @@ func TestNotificationProviderHandler_Preview_InvalidJSON(t *testing.T) { } func TestNotificationProviderHandler_Preview_WithData(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -591,7 +572,6 @@ func TestNotificationProviderHandler_Preview_WithData(t *testing.T) { } func TestNotificationProviderHandler_Preview_InvalidTemplate(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -616,7 +596,6 @@ func TestNotificationProviderHandler_Preview_InvalidTemplate(t *testing.T) { // Notification Template Handler Tests func TestNotificationTemplateHandler_List_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationTemplateHandler(svc) @@ -635,7 +614,6 @@ func TestNotificationTemplateHandler_List_Error(t *testing.T) { } func TestNotificationTemplateHandler_Create_BadJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationTemplateHandler(svc) @@ -652,7 +630,6 @@ func TestNotificationTemplateHandler_Create_BadJSON(t *testing.T) { } func TestNotificationTemplateHandler_Create_DBError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationTemplateHandler(svc) @@ -678,7 +655,6 @@ func TestNotificationTemplateHandler_Create_DBError(t *testing.T) { } func TestNotificationTemplateHandler_Update_BadJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationTemplateHandler(svc) @@ -696,7 +672,6 @@ func TestNotificationTemplateHandler_Update_BadJSON(t *testing.T) { } func TestNotificationTemplateHandler_Update_DBError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationTemplateHandler(svc) @@ -723,7 +698,6 @@ func TestNotificationTemplateHandler_Update_DBError(t *testing.T) { } func TestNotificationTemplateHandler_Delete_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationTemplateHandler(svc) @@ -743,7 +717,6 @@ func TestNotificationTemplateHandler_Delete_Error(t *testing.T) { } func TestNotificationTemplateHandler_Preview_BadJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationTemplateHandler(svc) @@ -760,7 +733,6 @@ func TestNotificationTemplateHandler_Preview_BadJSON(t *testing.T) { } func TestNotificationTemplateHandler_Preview_TemplateNotFound(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationTemplateHandler(svc) @@ -783,7 +755,6 @@ func TestNotificationTemplateHandler_Preview_TemplateNotFound(t *testing.T) { } func TestNotificationTemplateHandler_Preview_WithStoredTemplate(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationTemplateHandler(svc) @@ -815,7 +786,6 @@ func TestNotificationTemplateHandler_Preview_WithStoredTemplate(t *testing.T) { } func TestNotificationTemplateHandler_Preview_InvalidTemplate(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationTemplateHandler(svc) @@ -837,7 +807,6 @@ func TestNotificationTemplateHandler_Preview_InvalidTemplate(t *testing.T) { } func TestNotificationProviderHandler_Preview_TokenWriteOnly(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -861,7 +830,6 @@ func TestNotificationProviderHandler_Preview_TokenWriteOnly(t *testing.T) { } func TestNotificationProviderHandler_Update_TypeChangeRejected(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -895,7 +863,6 @@ func TestNotificationProviderHandler_Update_TypeChangeRejected(t *testing.T) { } func TestNotificationProviderHandler_Test_MissingProviderID(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -918,7 +885,6 @@ func TestNotificationProviderHandler_Test_MissingProviderID(t *testing.T) { } func TestNotificationProviderHandler_Test_ProviderNotFound(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -942,7 +908,6 @@ func TestNotificationProviderHandler_Test_ProviderNotFound(t *testing.T) { } func TestNotificationProviderHandler_Test_EmptyProviderURL(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -995,7 +960,6 @@ func TestIsProviderValidationError_Comprehensive(t *testing.T) { } func TestNotificationProviderHandler_Update_UnsupportedType(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -1028,7 +992,6 @@ func TestNotificationProviderHandler_Update_UnsupportedType(t *testing.T) { } func TestNotificationProviderHandler_Update_GotifyKeepsExistingToken(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) @@ -1066,7 +1029,6 @@ func TestNotificationProviderHandler_Update_GotifyKeepsExistingToken(t *testing. } func TestNotificationProviderHandler_Test_ReadDBError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationCoverageDB(t) svc := services.NewNotificationService(db, nil) h := NewNotificationProviderHandler(svc) diff --git a/backend/internal/api/handlers/notification_handler_test.go b/backend/internal/api/handlers/notification_handler_test.go index 6328acd54..e883536de 100644 --- a/backend/internal/api/handlers/notification_handler_test.go +++ b/backend/internal/api/handlers/notification_handler_test.go @@ -29,7 +29,6 @@ func setupNotificationTestDB(t *testing.T) *gorm.DB { } func TestNotificationHandler_List(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationTestDB(t) // Seed data @@ -65,7 +64,6 @@ func TestNotificationHandler_List(t *testing.T) { } func TestNotificationHandler_MarkAsRead(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationTestDB(t) // Seed data @@ -89,7 +87,6 @@ func TestNotificationHandler_MarkAsRead(t *testing.T) { } func TestNotificationHandler_MarkAllAsRead(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationTestDB(t) // Seed data @@ -113,7 +110,6 @@ func TestNotificationHandler_MarkAllAsRead(t *testing.T) { } func TestNotificationHandler_MarkAllAsRead_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationTestDB(t) service := services.NewNotificationService(db, nil) handler := handlers.NewNotificationHandler(service) @@ -132,7 +128,6 @@ func TestNotificationHandler_MarkAllAsRead_Error(t *testing.T) { } func TestNotificationHandler_DBError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupNotificationTestDB(t) service := services.NewNotificationService(db, nil) handler := handlers.NewNotificationHandler(service) diff --git a/backend/internal/api/handlers/notification_provider_blocker3_test.go b/backend/internal/api/handlers/notification_provider_blocker3_test.go index 5cd6338e2..28fa37725 100644 --- a/backend/internal/api/handlers/notification_provider_blocker3_test.go +++ b/backend/internal/api/handlers/notification_provider_blocker3_test.go @@ -17,7 +17,6 @@ import ( // TestBlocker3_CreateProviderValidationWithSecurityEvents verifies supported/unsupported provider handling with security events enabled. func TestBlocker3_CreateProviderRejectsNonDiscordWithSecurityEvents(t *testing.T) { - gin.SetMode(gin.TestMode) // Setup test database db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) @@ -89,7 +88,6 @@ func TestBlocker3_CreateProviderRejectsNonDiscordWithSecurityEvents(t *testing.T // TestBlocker3_CreateProviderAcceptsDiscordWithSecurityEvents tests that create accepts Discord providers with security events. func TestBlocker3_CreateProviderAcceptsDiscordWithSecurityEvents(t *testing.T) { - gin.SetMode(gin.TestMode) // Setup test database db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) @@ -137,7 +135,6 @@ func TestBlocker3_CreateProviderAcceptsDiscordWithSecurityEvents(t *testing.T) { // TestBlocker3_CreateProviderAcceptsNonDiscordWithoutSecurityEvents verifies webhook create without security events remains accepted. func TestBlocker3_CreateProviderAcceptsNonDiscordWithoutSecurityEvents(t *testing.T) { - gin.SetMode(gin.TestMode) // Setup test database db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) @@ -182,7 +179,6 @@ func TestBlocker3_CreateProviderAcceptsNonDiscordWithoutSecurityEvents(t *testin // TestBlocker3_UpdateProviderRejectsNonDiscordWithSecurityEvents verifies webhook update with security events is allowed in PR-1 scope. func TestBlocker3_UpdateProviderRejectsNonDiscordWithSecurityEvents(t *testing.T) { - gin.SetMode(gin.TestMode) // Setup test database db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) @@ -238,7 +234,6 @@ func TestBlocker3_UpdateProviderRejectsNonDiscordWithSecurityEvents(t *testing.T // TestBlocker3_UpdateProviderAcceptsDiscordWithSecurityEvents tests that update accepts Discord providers with security events. func TestBlocker3_UpdateProviderAcceptsDiscordWithSecurityEvents(t *testing.T) { - gin.SetMode(gin.TestMode) // Setup test database db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) @@ -295,7 +290,6 @@ func TestBlocker3_UpdateProviderAcceptsDiscordWithSecurityEvents(t *testing.T) { // TestBlocker3_MultipleSecurityEventsEnforcesDiscordOnly tests webhook remains accepted with security flags in PR-1 scope. func TestBlocker3_MultipleSecurityEventsEnforcesDiscordOnly(t *testing.T) { - gin.SetMode(gin.TestMode) // Setup test database db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) @@ -352,7 +346,6 @@ func TestBlocker3_MultipleSecurityEventsEnforcesDiscordOnly(t *testing.T) { // TestBlocker3_UpdateProvider_DatabaseError tests database error handling when fetching existing provider (lines 137-139). func TestBlocker3_UpdateProvider_DatabaseError(t *testing.T) { - gin.SetMode(gin.TestMode) // Setup test database db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) diff --git a/backend/internal/api/handlers/notification_provider_discord_only_test.go b/backend/internal/api/handlers/notification_provider_discord_only_test.go index 0a91d9f3b..77baed823 100644 --- a/backend/internal/api/handlers/notification_provider_discord_only_test.go +++ b/backend/internal/api/handlers/notification_provider_discord_only_test.go @@ -18,7 +18,6 @@ import ( // TestDiscordOnly_CreateRejectsNonDiscord verifies unsupported provider types are rejected while supported types are accepted. func TestDiscordOnly_CreateRejectsNonDiscord(t *testing.T) { - gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) @@ -81,7 +80,6 @@ func TestDiscordOnly_CreateRejectsNonDiscord(t *testing.T) { // TestDiscordOnly_CreateAcceptsDiscord tests that create accepts Discord providers. func TestDiscordOnly_CreateAcceptsDiscord(t *testing.T) { - gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) @@ -115,7 +113,6 @@ func TestDiscordOnly_CreateAcceptsDiscord(t *testing.T) { // TestDiscordOnly_UpdateRejectsTypeMutation tests that update blocks type mutation for deprecated providers. func TestDiscordOnly_UpdateRejectsTypeMutation(t *testing.T) { - gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) @@ -169,7 +166,6 @@ func TestDiscordOnly_UpdateRejectsTypeMutation(t *testing.T) { // TestDiscordOnly_UpdateRejectsEnable tests that update blocks enabling deprecated providers. func TestDiscordOnly_UpdateRejectsEnable(t *testing.T) { - gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) @@ -217,7 +213,6 @@ func TestDiscordOnly_UpdateRejectsEnable(t *testing.T) { // TestDiscordOnly_UpdateAllowsDisabledDeprecated tests that update allows updating disabled deprecated providers (except type/enable). func TestDiscordOnly_UpdateAllowsDisabledDeprecated(t *testing.T) { - gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) @@ -265,7 +260,6 @@ func TestDiscordOnly_UpdateAllowsDisabledDeprecated(t *testing.T) { // TestDiscordOnly_UpdateAcceptsDiscord tests that update accepts Discord provider updates. func TestDiscordOnly_UpdateAcceptsDiscord(t *testing.T) { - gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) @@ -313,7 +307,6 @@ func TestDiscordOnly_UpdateAcceptsDiscord(t *testing.T) { // TestDiscordOnly_DeleteAllowsDeprecated tests that delete works for deprecated providers. func TestDiscordOnly_DeleteAllowsDeprecated(t *testing.T) { - gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) @@ -405,7 +398,6 @@ func TestDiscordOnly_ErrorCodes(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) diff --git a/backend/internal/api/handlers/notification_provider_patch_coverage_test.go b/backend/internal/api/handlers/notification_provider_patch_coverage_test.go index 37be84670..94f3876b0 100644 --- a/backend/internal/api/handlers/notification_provider_patch_coverage_test.go +++ b/backend/internal/api/handlers/notification_provider_patch_coverage_test.go @@ -36,7 +36,6 @@ func TestUpdate_BlockTypeMutationForNonDiscord(t *testing.T) { service := services.NewNotificationService(db, nil) handler := NewNotificationProviderHandler(service) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -88,7 +87,6 @@ func TestUpdate_AllowTypeMutationForDiscord(t *testing.T) { service := services.NewNotificationService(db, nil) handler := NewNotificationProviderHandler(service) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") diff --git a/backend/internal/api/handlers/npm_import_handler_test.go b/backend/internal/api/handlers/npm_import_handler_test.go index e9fcc9aa6..ca7d96707 100644 --- a/backend/internal/api/handlers/npm_import_handler_test.go +++ b/backend/internal/api/handlers/npm_import_handler_test.go @@ -39,7 +39,6 @@ func TestNPMImportHandler_RegisterRoutes(t *testing.T) { db := setupNPMTestDB(t) handler := NewNPMImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) @@ -59,7 +58,6 @@ func TestNPMImportHandler_Upload_ValidNPMExport(t *testing.T) { db := setupNPMTestDB(t) handler := NewNPMImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) @@ -121,7 +119,6 @@ func TestNPMImportHandler_Upload_EmptyExport(t *testing.T) { db := setupNPMTestDB(t) handler := NewNPMImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) @@ -146,7 +143,6 @@ func TestNPMImportHandler_Upload_InvalidJSON(t *testing.T) { db := setupNPMTestDB(t) handler := NewNPMImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) @@ -177,7 +173,6 @@ func TestNPMImportHandler_Upload_ConflictDetection(t *testing.T) { handler := NewNPMImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) @@ -219,7 +214,6 @@ func TestNPMImportHandler_Commit_CreateNew(t *testing.T) { db := setupNPMTestDB(t) handler := NewNPMImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) @@ -288,7 +282,6 @@ func TestNPMImportHandler_Commit_SkipAction(t *testing.T) { db := setupNPMTestDB(t) handler := NewNPMImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) @@ -351,7 +344,6 @@ func TestNPMImportHandler_Commit_SessionNotFound(t *testing.T) { db := setupNPMTestDB(t) handler := NewNPMImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) @@ -382,7 +374,6 @@ func TestNPMImportHandler_Cancel(t *testing.T) { db := setupNPMTestDB(t) handler := NewNPMImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) @@ -457,7 +448,6 @@ func TestNPMImportHandler_Cancel_RequiresValidJSONBody(t *testing.T) { db := setupNPMTestDB(t) handler := NewNPMImportHandler(db) - gin.SetMode(gin.TestMode) router := gin.New() api := router.Group("/api/v1") handler.RegisterRoutes(api) diff --git a/backend/internal/api/handlers/plugin_handler_test.go b/backend/internal/api/handlers/plugin_handler_test.go index 2a00812fb..fab63f435 100644 --- a/backend/internal/api/handlers/plugin_handler_test.go +++ b/backend/internal/api/handlers/plugin_handler_test.go @@ -32,7 +32,6 @@ func TestPluginHandler_NewPluginHandler(t *testing.T) { } func TestPluginHandler_ListPlugins(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -82,7 +81,6 @@ func TestPluginHandler_ListPlugins(t *testing.T) { } func TestPluginHandler_GetPlugin_InvalidID(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) handler := NewPluginHandler(db, pluginLoader) @@ -99,7 +97,6 @@ func TestPluginHandler_GetPlugin_InvalidID(t *testing.T) { } func TestPluginHandler_GetPlugin_NotFound(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) handler := NewPluginHandler(db, pluginLoader) @@ -116,7 +113,6 @@ func TestPluginHandler_GetPlugin_NotFound(t *testing.T) { } func TestPluginHandler_GetPlugin_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -152,7 +148,6 @@ func TestPluginHandler_GetPlugin_Success(t *testing.T) { } func TestPluginHandler_EnablePlugin_InvalidID(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) handler := NewPluginHandler(db, pluginLoader) @@ -168,7 +163,6 @@ func TestPluginHandler_EnablePlugin_InvalidID(t *testing.T) { } func TestPluginHandler_EnablePlugin_NotFound(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) handler := NewPluginHandler(db, pluginLoader) @@ -184,7 +178,6 @@ func TestPluginHandler_EnablePlugin_NotFound(t *testing.T) { } func TestPluginHandler_EnablePlugin_AlreadyEnabled(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -212,7 +205,6 @@ func TestPluginHandler_EnablePlugin_AlreadyEnabled(t *testing.T) { } func TestPluginHandler_EnablePlugin_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -245,7 +237,6 @@ func TestPluginHandler_EnablePlugin_Success(t *testing.T) { } func TestPluginHandler_DisablePlugin_InvalidID(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) handler := NewPluginHandler(db, pluginLoader) @@ -261,7 +252,6 @@ func TestPluginHandler_DisablePlugin_InvalidID(t *testing.T) { } func TestPluginHandler_DisablePlugin_NotFound(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) handler := NewPluginHandler(db, pluginLoader) @@ -277,7 +267,6 @@ func TestPluginHandler_DisablePlugin_NotFound(t *testing.T) { } func TestPluginHandler_DisablePlugin_AlreadyDisabled(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -309,7 +298,6 @@ func TestPluginHandler_DisablePlugin_AlreadyDisabled(t *testing.T) { } func TestPluginHandler_DisablePlugin_InUse(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -346,7 +334,6 @@ func TestPluginHandler_DisablePlugin_InUse(t *testing.T) { } func TestPluginHandler_DisablePlugin_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -378,7 +365,6 @@ func TestPluginHandler_DisablePlugin_Success(t *testing.T) { } func TestPluginHandler_ReloadPlugins_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) pluginLoader := services.NewPluginLoaderService(db, "/nonexistent/plugins", nil) handler := NewPluginHandler(db, pluginLoader) @@ -397,7 +383,6 @@ func TestPluginHandler_ReloadPlugins_Success(t *testing.T) { // TestPluginHandler_ListPlugins_WithBuiltInProviders tests listing when built-in providers are registered func TestPluginHandler_ListPlugins_WithBuiltInProviders(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -502,7 +487,6 @@ func (m *mockDNSProvider) PollingInterval() time.Duration { // ============================================================================= func TestPluginHandler_ListPlugins_ExternalLoadedPlugin(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -570,7 +554,6 @@ func TestPluginHandler_ListPlugins_ExternalLoadedPlugin(t *testing.T) { } func TestPluginHandler_GetPlugin_WithProvider(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -619,7 +602,6 @@ func TestPluginHandler_GetPlugin_WithProvider(t *testing.T) { } func TestPluginHandler_EnablePlugin_WithLoadError(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/nonexistent/plugins", nil) @@ -663,7 +645,6 @@ func TestPluginHandler_EnablePlugin_WithLoadError(t *testing.T) { } func TestPluginHandler_DisablePlugin_WithUnloadError(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -698,7 +679,6 @@ func TestPluginHandler_DisablePlugin_WithUnloadError(t *testing.T) { } func TestPluginHandler_DisablePlugin_MultipleProviders(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -741,7 +721,6 @@ func TestPluginHandler_DisablePlugin_MultipleProviders(t *testing.T) { } func TestPluginHandler_ReloadPlugins_WithErrors(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) // Create a regular file and use it as pluginDir to force os.ReadDir error deterministically. @@ -763,7 +742,6 @@ func TestPluginHandler_ReloadPlugins_WithErrors(t *testing.T) { } func TestPluginHandler_ListPlugins_FailedPluginWithLoadedAt(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -813,7 +791,6 @@ func TestPluginHandler_ListPlugins_FailedPluginWithLoadedAt(t *testing.T) { } func TestPluginHandler_GetPlugin_WithLoadedAt(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -868,7 +845,6 @@ func TestPluginHandler_Count(t *testing.T) { // TestPluginHandler_EnablePlugin_DBUpdateError tests DB error when updating plugin enabled status func TestPluginHandler_EnablePlugin_DBUpdateError(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -901,7 +877,6 @@ func TestPluginHandler_EnablePlugin_DBUpdateError(t *testing.T) { // TestPluginHandler_DisablePlugin_DBUpdateError tests DB error when updating plugin disabled status func TestPluginHandler_DisablePlugin_DBUpdateError(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -934,7 +909,6 @@ func TestPluginHandler_DisablePlugin_DBUpdateError(t *testing.T) { // TestPluginHandler_GetPlugin_DBInternalError tests DB internal error when getting a plugin func TestPluginHandler_GetPlugin_DBInternalError(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -968,7 +942,6 @@ func TestPluginHandler_GetPlugin_DBInternalError(t *testing.T) { // TestPluginHandler_EnablePlugin_FirstDBLookupError tests DB error in first plugin lookup func TestPluginHandler_EnablePlugin_FirstDBLookupError(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -1002,7 +975,6 @@ func TestPluginHandler_EnablePlugin_FirstDBLookupError(t *testing.T) { // TestPluginHandler_DisablePlugin_FirstDBLookupError tests DB error in first plugin lookup during disable func TestPluginHandler_DisablePlugin_FirstDBLookupError(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) diff --git a/backend/internal/api/handlers/pr_coverage_test.go b/backend/internal/api/handlers/pr_coverage_test.go index 62a195c28..8770ded58 100644 --- a/backend/internal/api/handlers/pr_coverage_test.go +++ b/backend/internal/api/handlers/pr_coverage_test.go @@ -26,7 +26,6 @@ import ( // ============================================================================= func TestPluginHandler_EnablePlugin_DatabaseUpdateError(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -58,7 +57,6 @@ func TestPluginHandler_EnablePlugin_DatabaseUpdateError(t *testing.T) { } func TestPluginHandler_DisablePlugin_DatabaseUpdateError(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -90,7 +88,6 @@ func TestPluginHandler_DisablePlugin_DatabaseUpdateError(t *testing.T) { } func TestPluginHandler_GetPlugin_DatabaseError(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -122,7 +119,6 @@ func TestPluginHandler_GetPlugin_DatabaseError(t *testing.T) { } func TestPluginHandler_EnablePlugin_DatabaseFirstError(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -144,7 +140,6 @@ func TestPluginHandler_EnablePlugin_DatabaseFirstError(t *testing.T) { } func TestPluginHandler_DisablePlugin_DatabaseFirstError(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil) @@ -170,7 +165,6 @@ func TestPluginHandler_DisablePlugin_DatabaseFirstError(t *testing.T) { // ============================================================================= func TestEncryptionHandler_Validate_NonAdminAccess(t *testing.T) { - gin.SetMode(gin.TestMode) currentKey, _ := crypto.GenerateNewKey() require.NoError(t, os.Setenv("CHARON_ENCRYPTION_KEY", currentKey)) @@ -192,7 +186,6 @@ func TestEncryptionHandler_Validate_NonAdminAccess(t *testing.T) { } func TestEncryptionHandler_GetHistory_PaginationBoundary(t *testing.T) { - gin.SetMode(gin.TestMode) currentKey, _ := crypto.GenerateNewKey() require.NoError(t, os.Setenv("CHARON_ENCRYPTION_KEY", currentKey)) @@ -227,7 +220,6 @@ func TestEncryptionHandler_GetHistory_PaginationBoundary(t *testing.T) { } func TestEncryptionHandler_GetStatus_VersionInfo(t *testing.T) { - gin.SetMode(gin.TestMode) currentKey, _ := crypto.GenerateNewKey() require.NoError(t, os.Setenv("CHARON_ENCRYPTION_KEY", currentKey)) @@ -261,7 +253,6 @@ func TestEncryptionHandler_GetStatus_VersionInfo(t *testing.T) { // ============================================================================= func TestSettingsHandler_TestPublicURL_RoleNotExists(t *testing.T) { - gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{}) require.NoError(t, err) @@ -282,7 +273,6 @@ func TestSettingsHandler_TestPublicURL_RoleNotExists(t *testing.T) { } func TestSettingsHandler_TestPublicURL_InvalidURLFormat(t *testing.T) { - gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{}) require.NoError(t, err) @@ -306,7 +296,6 @@ func TestSettingsHandler_TestPublicURL_InvalidURLFormat(t *testing.T) { } func TestSettingsHandler_TestPublicURL_PrivateIPBlocked_Coverage(t *testing.T) { - gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{}) require.NoError(t, err) @@ -335,7 +324,6 @@ func TestSettingsHandler_TestPublicURL_PrivateIPBlocked_Coverage(t *testing.T) { } func TestSettingsHandler_ValidatePublicURL_WithTrailingSlash(t *testing.T) { - gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{}) require.NoError(t, err) @@ -363,7 +351,6 @@ func TestSettingsHandler_ValidatePublicURL_WithTrailingSlash(t *testing.T) { } func TestSettingsHandler_ValidatePublicURL_MissingScheme(t *testing.T) { - gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{}) require.NoError(t, err) @@ -395,7 +382,6 @@ func TestSettingsHandler_ValidatePublicURL_MissingScheme(t *testing.T) { // ============================================================================= func TestAuditLogHandler_List_PaginationEdgeCases(t *testing.T) { - gin.SetMode(gin.TestMode) dbPath := fmt.Sprintf("/tmp/test_audit_pagination_%d.db", time.Now().UnixNano()) t.Cleanup(func() { _ = os.Remove(dbPath) }) @@ -429,7 +415,6 @@ func TestAuditLogHandler_List_PaginationEdgeCases(t *testing.T) { } func TestAuditLogHandler_List_CategoryFilter(t *testing.T) { - gin.SetMode(gin.TestMode) dbPath := fmt.Sprintf("/tmp/test_audit_category_%d.db", time.Now().UnixNano()) t.Cleanup(func() { _ = os.Remove(dbPath) }) @@ -467,7 +452,6 @@ func TestAuditLogHandler_List_CategoryFilter(t *testing.T) { } func TestAuditLogHandler_ListByProvider_DatabaseError(t *testing.T) { - gin.SetMode(gin.TestMode) dbPath := fmt.Sprintf("/tmp/test_audit_db_error_%d.db", time.Now().UnixNano()) t.Cleanup(func() { _ = os.Remove(dbPath) }) @@ -494,7 +478,6 @@ func TestAuditLogHandler_ListByProvider_DatabaseError(t *testing.T) { } func TestAuditLogHandler_ListByProvider_InvalidProviderID(t *testing.T) { - gin.SetMode(gin.TestMode) dbPath := fmt.Sprintf("/tmp/test_audit_invalid_id_%d.db", time.Now().UnixNano()) t.Cleanup(func() { _ = os.Remove(dbPath) }) @@ -521,7 +504,6 @@ func TestAuditLogHandler_ListByProvider_InvalidProviderID(t *testing.T) { // ============================================================================= func TestGetActorFromGinContext_InvalidUserIDType(t *testing.T) { - gin.SetMode(gin.TestMode) router := gin.New() var capturedActor string @@ -547,7 +529,6 @@ func TestGetActorFromGinContext_InvalidUserIDType(t *testing.T) { // ============================================================================= func TestIsAdmin_NonAdminRole(t *testing.T) { - gin.SetMode(gin.TestMode) router := gin.New() router.Use(func(c *gin.Context) { @@ -577,7 +558,6 @@ func setupCredentialHandlerTestWithCtx(t *testing.T) (*gin.Engine, *gorm.DB, *mo require.NoError(t, os.Setenv("CHARON_ENCRYPTION_KEY", "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=")) t.Cleanup(func() { require.NoError(t, os.Unsetenv("CHARON_ENCRYPTION_KEY")) }) - gin.SetMode(gin.TestMode) router := gin.New() dbName := fmt.Sprintf("file:%s?mode=memory&cache=shared&_journal_mode=WAL", t.Name()) @@ -679,7 +659,6 @@ func TestCredentialHandler_List_DatabaseClosed(t *testing.T) { require.NoError(t, os.Setenv("CHARON_ENCRYPTION_KEY", "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=")) defer func() { require.NoError(t, os.Unsetenv("CHARON_ENCRYPTION_KEY")) }() - gin.SetMode(gin.TestMode) router := gin.New() dbName := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) @@ -820,7 +799,6 @@ func TestCredentialHandler_EnableMultiCredentials_BadProviderID(t *testing.T) { // ============================================================================= func TestEncryptionHandler_Validate_AdminSuccess(t *testing.T) { - gin.SetMode(gin.TestMode) currentKey, _ := crypto.GenerateNewKey() require.NoError(t, os.Setenv("CHARON_ENCRYPTION_KEY", currentKey)) diff --git a/backend/internal/api/handlers/proxy_host_handler_update_test.go b/backend/internal/api/handlers/proxy_host_handler_update_test.go index 3282ee173..d6d692eee 100644 --- a/backend/internal/api/handlers/proxy_host_handler_update_test.go +++ b/backend/internal/api/handlers/proxy_host_handler_update_test.go @@ -23,7 +23,6 @@ import ( // Uses a dedicated in-memory SQLite database with all required models migrated. func setupUpdateTestRouter(t *testing.T) (*gin.Engine, *gorm.DB) { t.Helper() - gin.SetMode(gin.TestMode) dsn := "file:" + t.Name() + "?mode=memory&cache=shared" db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) @@ -951,7 +950,6 @@ func TestProxyHostUpdate_SecurityHeaderProfileID_SetToNull(t *testing.T) { // (other than not found) during profile lookup returns a 500 Internal Server Error. func TestBulkUpdateSecurityHeaders_DBError_NonNotFound(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) dsn := "file:" + t.Name() + "?mode=memory&cache=shared" db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) diff --git a/backend/internal/api/handlers/security_event_intake_test.go b/backend/internal/api/handlers/security_event_intake_test.go index 010a530cc..0a65f4cf9 100644 --- a/backend/internal/api/handlers/security_event_intake_test.go +++ b/backend/internal/api/handlers/security_event_intake_test.go @@ -59,7 +59,6 @@ func TestSecurityEventIntakeAuthLocalhost(t *testing.T) { managementCIDRs, ) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -100,7 +99,6 @@ func TestSecurityEventIntakeAuthManagementCIDR(t *testing.T) { managementCIDRs, ) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -141,7 +139,6 @@ func TestSecurityEventIntakeAuthUnauthorizedIP(t *testing.T) { managementCIDRs, ) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -187,7 +184,6 @@ func TestSecurityEventIntakeAuthInvalidIP(t *testing.T) { managementCIDRs, ) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -246,7 +242,6 @@ func TestSecurityEventIntakeDispatchInvoked(t *testing.T) { managementCIDRs, ) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -315,7 +310,6 @@ func TestSecurityEventIntakeR6Intact(t *testing.T) { service := services.NewEnhancedSecurityNotificationService(db) handler := NewSecurityNotificationHandler(service) - gin.SetMode(gin.TestMode) router := gin.New() // Add auth middleware that sets user context @@ -386,7 +380,6 @@ func TestSecurityEventIntakeDiscordOnly(t *testing.T) { managementCIDRs, ) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -431,7 +424,6 @@ func TestSecurityEventIntakeMalformedPayload(t *testing.T) { managementCIDRs, ) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -466,7 +458,6 @@ func TestSecurityEventIntakeIPv6Localhost(t *testing.T) { managementCIDRs, ) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) diff --git a/backend/internal/api/handlers/security_geoip_endpoints_test.go b/backend/internal/api/handlers/security_geoip_endpoints_test.go index 7d79f2afc..b29f5d1c2 100644 --- a/backend/internal/api/handlers/security_geoip_endpoints_test.go +++ b/backend/internal/api/handlers/security_geoip_endpoints_test.go @@ -15,7 +15,6 @@ import ( ) func TestSecurityHandler_GetGeoIPStatus_NotInitialized(t *testing.T) { - gin.SetMode(gin.TestMode) h := NewSecurityHandler(config.SecurityConfig{}, nil, nil) r := gin.New() @@ -34,7 +33,6 @@ func TestSecurityHandler_GetGeoIPStatus_NotInitialized(t *testing.T) { } func TestSecurityHandler_GetGeoIPStatus_Initialized_NotLoaded(t *testing.T) { - gin.SetMode(gin.TestMode) h := NewSecurityHandler(config.SecurityConfig{}, nil, nil) h.SetGeoIPService(&services.GeoIPService{}) @@ -55,7 +53,6 @@ func TestSecurityHandler_GetGeoIPStatus_Initialized_NotLoaded(t *testing.T) { } func TestSecurityHandler_ReloadGeoIP_NotInitialized(t *testing.T) { - gin.SetMode(gin.TestMode) h := NewSecurityHandler(config.SecurityConfig{}, nil, nil) r := gin.New() @@ -73,7 +70,6 @@ func TestSecurityHandler_ReloadGeoIP_NotInitialized(t *testing.T) { } func TestSecurityHandler_ReloadGeoIP_LoadError(t *testing.T) { - gin.SetMode(gin.TestMode) h := NewSecurityHandler(config.SecurityConfig{}, nil, nil) h.SetGeoIPService(&services.GeoIPService{}) // dbPath empty => Load() will error @@ -94,7 +90,6 @@ func TestSecurityHandler_ReloadGeoIP_LoadError(t *testing.T) { } func TestSecurityHandler_LookupGeoIP_MissingIPAddress(t *testing.T) { - gin.SetMode(gin.TestMode) h := NewSecurityHandler(config.SecurityConfig{}, nil, nil) r := gin.New() @@ -115,7 +110,6 @@ func TestSecurityHandler_LookupGeoIP_MissingIPAddress(t *testing.T) { } func TestSecurityHandler_LookupGeoIP_ServiceUnavailable(t *testing.T) { - gin.SetMode(gin.TestMode) h := NewSecurityHandler(config.SecurityConfig{}, nil, nil) h.SetGeoIPService(&services.GeoIPService{}) // present but not loaded diff --git a/backend/internal/api/handlers/security_handler.go b/backend/internal/api/handlers/security_handler.go index 20dafef9e..591c2f2be 100644 --- a/backend/internal/api/handlers/security_handler.go +++ b/backend/internal/api/handlers/security_handler.go @@ -34,6 +34,17 @@ type WAFExclusion struct { Description string `json:"description,omitempty"` } +// CreateDecisionRequest is the client-facing DTO for manual decision creation. +// Enrichment fields (Scenario, Country, ExpiresAt) are system-populated and +// deliberately excluded to prevent clients from injecting arbitrary values. +type CreateDecisionRequest struct { + IP string `json:"ip" binding:"required"` + Action string `json:"action" binding:"required"` + Host string `json:"host,omitempty"` + RuleID string `json:"rule_id,omitempty"` + Details string `json:"details,omitempty"` +} + // SecurityHandler handles security-related API requests. type SecurityHandler struct { cfg config.SecurityConfig @@ -328,19 +339,19 @@ func (h *SecurityHandler) CreateDecision(c *gin.Context) { return } - var payload models.SecurityDecision - if err := c.ShouldBindJSON(&payload); err != nil { + var req CreateDecisionRequest + if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) return } - if payload.IP == "" || payload.Action == "" { + if req.IP == "" || req.Action == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "ip and action are required"}) return } // CRITICAL: Validate IP format to prevent SQL injection via IP field // Must accept both single IPs and CIDR ranges - if !isValidIP(payload.IP) && !isValidCIDR(payload.IP) { + if !isValidIP(req.IP) && !isValidCIDR(req.IP) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid IP address format"}) return } @@ -348,16 +359,21 @@ func (h *SecurityHandler) CreateDecision(c *gin.Context) { // CRITICAL: Validate action enum // Only accept known action types to prevent injection via action field validActions := []string{"block", "allow", "captcha"} - if !contains(validActions, payload.Action) { + if !contains(validActions, req.Action) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid action"}) return } - // Sanitize details field (limit length, strip control characters) - payload.Details = sanitizeString(payload.Details, 1000) - - // Populate source - payload.Source = "manual" + // Map DTO to model — enrichment fields (Scenario, Country, ExpiresAt) + // are intentionally excluded; they are system-populated only. + payload := models.SecurityDecision{ + IP: req.IP, + Action: req.Action, + Host: req.Host, + RuleID: req.RuleID, + Details: sanitizeString(req.Details, 1000), + Source: "manual", + } if err := h.svc.LogDecision(&payload); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to log decision"}) return diff --git a/backend/internal/api/handlers/security_handler_audit_test.go b/backend/internal/api/handlers/security_handler_audit_test.go index 47d13c2fe..e2182de99 100644 --- a/backend/internal/api/handlers/security_handler_audit_test.go +++ b/backend/internal/api/handlers/security_handler_audit_test.go @@ -56,7 +56,6 @@ func setupAuditTestDB(t *testing.T) *gorm.DB { // ============================================================================= func TestSecurityHandler_GetStatus_SQLInjection(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditTestDB(t) // Seed malicious setting keys that could be used in SQL injection @@ -93,7 +92,6 @@ func TestSecurityHandler_GetStatus_SQLInjection(t *testing.T) { } func TestSecurityHandler_CreateDecision_SQLInjection(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditTestDB(t) cfg := config.SecurityConfig{} @@ -140,7 +138,6 @@ func TestSecurityHandler_CreateDecision_SQLInjection(t *testing.T) { // ============================================================================= func TestSecurityHandler_UpsertRuleSet_MassivePayload(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditTestDB(t) cfg := config.SecurityConfig{} @@ -176,7 +173,6 @@ func TestSecurityHandler_UpsertRuleSet_MassivePayload(t *testing.T) { } func TestSecurityHandler_UpsertRuleSet_EmptyName(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditTestDB(t) cfg := config.SecurityConfig{} @@ -208,7 +204,6 @@ func TestSecurityHandler_UpsertRuleSet_EmptyName(t *testing.T) { } func TestSecurityHandler_CreateDecision_EmptyFields(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditTestDB(t) cfg := config.SecurityConfig{} @@ -250,7 +245,6 @@ func TestSecurityHandler_CreateDecision_EmptyFields(t *testing.T) { // ============================================================================= func TestSecurityHandler_GetStatus_SettingsOverride(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditTestDB(t) // Create SecurityConfig with all security features enabled (DB priority) @@ -308,7 +302,6 @@ func TestSecurityHandler_GetStatus_SettingsOverride(t *testing.T) { } func TestSecurityHandler_GetStatus_DisabledViaSettings(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditTestDB(t) // Seed settings that disable everything @@ -356,7 +349,6 @@ func TestSecurityHandler_GetStatus_DisabledViaSettings(t *testing.T) { // ============================================================================= func TestSecurityAudit_DeleteRuleSet_InvalidID(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditTestDB(t) cfg := config.SecurityConfig{} @@ -401,7 +393,6 @@ func TestSecurityAudit_DeleteRuleSet_InvalidID(t *testing.T) { // ============================================================================= func TestSecurityHandler_UpsertRuleSet_XSSInContent(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditTestDB(t) cfg := config.SecurityConfig{} @@ -450,7 +441,6 @@ func TestSecurityHandler_UpsertRuleSet_XSSInContent(t *testing.T) { // ============================================================================= func TestSecurityHandler_UpdateConfig_RateLimitBounds(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditTestDB(t) cfg := config.SecurityConfig{} @@ -512,7 +502,6 @@ func TestSecurityHandler_UpdateConfig_RateLimitBounds(t *testing.T) { // ============================================================================= func TestSecurityHandler_GetStatus_NilDB(t *testing.T) { - gin.SetMode(gin.TestMode) // Handler with nil DB should not panic cfg := config.SecurityConfig{CerberusEnabled: true} @@ -537,7 +526,6 @@ func TestSecurityHandler_GetStatus_NilDB(t *testing.T) { // ============================================================================= func TestSecurityHandler_Enable_WithoutWhitelist(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditTestDB(t) // Create config without whitelist @@ -564,7 +552,6 @@ func TestSecurityHandler_Enable_WithoutWhitelist(t *testing.T) { } func TestSecurityHandler_Disable_RequiresToken(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditTestDB(t) // Create config with break-glass hash @@ -592,7 +579,6 @@ func TestSecurityHandler_Disable_RequiresToken(t *testing.T) { // ============================================================================= func TestSecurityHandler_GetStatus_CrowdSecModeValidation(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupAuditTestDB(t) // Try to set invalid CrowdSec modes via settings diff --git a/backend/internal/api/handlers/security_handler_authz_test.go b/backend/internal/api/handlers/security_handler_authz_test.go index 32c6bf8a8..4e1d314c2 100644 --- a/backend/internal/api/handlers/security_handler_authz_test.go +++ b/backend/internal/api/handlers/security_handler_authz_test.go @@ -15,7 +15,6 @@ import ( ) func TestSecurityHandler_MutatorsRequireAdmin(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityRuleSet{}, &models.SecurityDecision{}, &models.SecurityAudit{})) diff --git a/backend/internal/api/handlers/security_handler_cache_test.go b/backend/internal/api/handlers/security_handler_cache_test.go index 96bbe96b3..44a8c1130 100644 --- a/backend/internal/api/handlers/security_handler_cache_test.go +++ b/backend/internal/api/handlers/security_handler_cache_test.go @@ -21,7 +21,6 @@ func (t *testCacheInvalidator) InvalidateCache() { } func TestSecurityHandler_ToggleSecurityModule_InvalidatesCache(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{})) diff --git a/backend/internal/api/handlers/security_handler_clean_test.go b/backend/internal/api/handlers/security_handler_clean_test.go index 5019a34b2..771a66acb 100644 --- a/backend/internal/api/handlers/security_handler_clean_test.go +++ b/backend/internal/api/handlers/security_handler_clean_test.go @@ -28,7 +28,6 @@ func setupTestDB(t *testing.T) *gorm.DB { } func TestSecurityHandler_GetStatus_Clean(t *testing.T) { - gin.SetMode(gin.TestMode) // Basic disabled scenario cfg := config.SecurityConfig{ @@ -54,7 +53,6 @@ func TestSecurityHandler_GetStatus_Clean(t *testing.T) { } func TestSecurityHandler_Cerberus_DBOverride(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) // set DB to enable cerberus @@ -80,7 +78,6 @@ func TestSecurityHandler_Cerberus_DBOverride(t *testing.T) { } func TestSecurityHandler_ACL_DBOverride(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) // set DB to enable ACL (override config) @@ -116,7 +113,6 @@ func TestSecurityHandler_ACL_DBOverride(t *testing.T) { } func TestSecurityHandler_GenerateBreakGlass_ReturnsToken(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() @@ -139,7 +135,6 @@ func TestSecurityHandler_GenerateBreakGlass_ReturnsToken(t *testing.T) { } func TestSecurityHandler_ACL_DisabledWhenCerberusOff(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) // set DB to enable ACL but disable Cerberus @@ -171,7 +166,6 @@ func TestSecurityHandler_ACL_DisabledWhenCerberusOff(t *testing.T) { } func TestSecurityHandler_CrowdSec_Mode_DBOverride(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) // set DB to configure crowdsec.mode to local @@ -197,7 +191,6 @@ func TestSecurityHandler_CrowdSec_Mode_DBOverride(t *testing.T) { } func TestSecurityHandler_CrowdSec_ExternalMappedToDisabled_DBOverride(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) // set DB to configure crowdsec.mode to external if err := db.Create(&models.Setting{Key: "security.crowdsec.mode", Value: "unknown"}).Error; err != nil { @@ -221,7 +214,6 @@ func TestSecurityHandler_CrowdSec_ExternalMappedToDisabled_DBOverride(t *testing } func TestSecurityHandler_ExternalModeMappedToDisabled(t *testing.T) { - gin.SetMode(gin.TestMode) cfg := config.SecurityConfig{ CrowdSecMode: "unknown", WAFMode: "disabled", @@ -245,7 +237,6 @@ func TestSecurityHandler_ExternalModeMappedToDisabled(t *testing.T) { } func TestSecurityHandler_Enable_Disable_WithAdminWhitelistAndToken(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) // Add SecurityConfig with no admin whitelist - should refuse enable sec := models.SecurityConfig{Name: "default", Enabled: false, AdminWhitelist: ""} diff --git a/backend/internal/api/handlers/security_handler_coverage_test.go b/backend/internal/api/handlers/security_handler_coverage_test.go index 7ab25de7b..71ee9415e 100644 --- a/backend/internal/api/handlers/security_handler_coverage_test.go +++ b/backend/internal/api/handlers/security_handler_coverage_test.go @@ -21,7 +21,6 @@ import ( // Tests for UpdateConfig handler to improve coverage (currently 46%) func TestSecurityHandler_UpdateConfig_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityRuleSet{}, &models.SecurityDecision{}, &models.SecurityAudit{})) @@ -53,7 +52,6 @@ func TestSecurityHandler_UpdateConfig_Success(t *testing.T) { } func TestSecurityHandler_UpdateConfig_DefaultName(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityRuleSet{}, &models.SecurityDecision{}, &models.SecurityAudit{})) @@ -80,7 +78,6 @@ func TestSecurityHandler_UpdateConfig_DefaultName(t *testing.T) { } func TestSecurityHandler_UpdateConfig_InvalidPayload(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -102,7 +99,6 @@ func TestSecurityHandler_UpdateConfig_InvalidPayload(t *testing.T) { // Tests for GetConfig handler func TestSecurityHandler_GetConfig_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -126,7 +122,6 @@ func TestSecurityHandler_GetConfig_Success(t *testing.T) { } func TestSecurityHandler_GetConfig_NotFound(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -147,7 +142,6 @@ func TestSecurityHandler_GetConfig_NotFound(t *testing.T) { // Tests for ListDecisions handler func TestSecurityHandler_ListDecisions_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityDecision{})) @@ -172,7 +166,6 @@ func TestSecurityHandler_ListDecisions_Success(t *testing.T) { } func TestSecurityHandler_ListDecisions_WithLimit(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityDecision{})) @@ -199,7 +192,6 @@ func TestSecurityHandler_ListDecisions_WithLimit(t *testing.T) { // Tests for CreateDecision handler func TestSecurityHandler_CreateDecision_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityAudit{})) @@ -228,7 +220,6 @@ func TestSecurityHandler_CreateDecision_Success(t *testing.T) { } func TestSecurityHandler_CreateDecision_MissingIP(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityDecision{})) @@ -254,7 +245,6 @@ func TestSecurityHandler_CreateDecision_MissingIP(t *testing.T) { } func TestSecurityHandler_CreateDecision_MissingAction(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityDecision{})) @@ -280,7 +270,6 @@ func TestSecurityHandler_CreateDecision_MissingAction(t *testing.T) { } func TestSecurityHandler_CreateDecision_InvalidPayload(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityDecision{})) @@ -302,7 +291,6 @@ func TestSecurityHandler_CreateDecision_InvalidPayload(t *testing.T) { // Tests for ListRuleSets handler func TestSecurityHandler_ListRuleSets_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{})) @@ -328,7 +316,6 @@ func TestSecurityHandler_ListRuleSets_Success(t *testing.T) { // Tests for UpsertRuleSet handler func TestSecurityHandler_UpsertRuleSet_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{}, &models.SecurityAudit{})) @@ -356,7 +343,6 @@ func TestSecurityHandler_UpsertRuleSet_Success(t *testing.T) { } func TestSecurityHandler_UpsertRuleSet_MissingName(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{})) @@ -383,7 +369,6 @@ func TestSecurityHandler_UpsertRuleSet_MissingName(t *testing.T) { } func TestSecurityHandler_UpsertRuleSet_InvalidPayload(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{})) @@ -405,7 +390,6 @@ func TestSecurityHandler_UpsertRuleSet_InvalidPayload(t *testing.T) { // Tests for DeleteRuleSet handler (currently 52%) func TestSecurityHandler_DeleteRuleSet_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{}, &models.SecurityAudit{})) @@ -433,7 +417,6 @@ func TestSecurityHandler_DeleteRuleSet_Success(t *testing.T) { } func TestSecurityHandler_DeleteRuleSet_NotFound(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{})) @@ -453,7 +436,6 @@ func TestSecurityHandler_DeleteRuleSet_NotFound(t *testing.T) { } func TestSecurityHandler_DeleteRuleSet_InvalidID(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{})) @@ -473,7 +455,6 @@ func TestSecurityHandler_DeleteRuleSet_InvalidID(t *testing.T) { } func TestSecurityHandler_DeleteRuleSet_EmptyID(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{})) @@ -497,7 +478,6 @@ func TestSecurityHandler_DeleteRuleSet_EmptyID(t *testing.T) { // Tests for Enable handler func TestSecurityHandler_Enable_NoConfigNoWhitelist(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -515,7 +495,6 @@ func TestSecurityHandler_Enable_NoConfigNoWhitelist(t *testing.T) { } func TestSecurityHandler_Enable_WithWhitelist(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -537,7 +516,6 @@ func TestSecurityHandler_Enable_WithWhitelist(t *testing.T) { } func TestSecurityHandler_Enable_IPNotInWhitelist(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -559,7 +537,6 @@ func TestSecurityHandler_Enable_IPNotInWhitelist(t *testing.T) { } func TestSecurityHandler_Enable_WithValidBreakGlassToken(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -600,7 +577,6 @@ func TestSecurityHandler_Enable_WithValidBreakGlassToken(t *testing.T) { } func TestSecurityHandler_Enable_WithInvalidBreakGlassToken(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -625,7 +601,6 @@ func TestSecurityHandler_Enable_WithInvalidBreakGlassToken(t *testing.T) { // Tests for Disable handler (currently 44%) func TestSecurityHandler_Disable_FromLocalhost(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -654,7 +629,6 @@ func TestSecurityHandler_Disable_FromLocalhost(t *testing.T) { } func TestSecurityHandler_Disable_FromRemoteWithToken(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -695,7 +669,6 @@ func TestSecurityHandler_Disable_FromRemoteWithToken(t *testing.T) { } func TestSecurityHandler_Disable_FromRemoteNoToken(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -719,7 +692,6 @@ func TestSecurityHandler_Disable_FromRemoteNoToken(t *testing.T) { } func TestSecurityHandler_Disable_FromRemoteInvalidToken(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -747,7 +719,6 @@ func TestSecurityHandler_Disable_FromRemoteInvalidToken(t *testing.T) { // Tests for GenerateBreakGlass handler func TestSecurityHandler_GenerateBreakGlass_NoConfig(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -773,7 +744,6 @@ func TestSecurityHandler_GenerateBreakGlass_NoConfig(t *testing.T) { // Test Enable with IPv6 localhost func TestSecurityHandler_Disable_FromIPv6Localhost(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -798,7 +768,6 @@ func TestSecurityHandler_Disable_FromIPv6Localhost(t *testing.T) { // Test Enable with CIDR whitelist matching func TestSecurityHandler_Enable_WithCIDRWhitelist(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -821,7 +790,6 @@ func TestSecurityHandler_Enable_WithCIDRWhitelist(t *testing.T) { // Test Enable with exact IP in whitelist func TestSecurityHandler_Enable_WithExactIPWhitelist(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -843,7 +811,6 @@ func TestSecurityHandler_Enable_WithExactIPWhitelist(t *testing.T) { } func TestSecurityHandler_GetStatus_BackwardCompatibilityOverrides(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{}, &models.CaddyConfig{})) @@ -887,7 +854,6 @@ func TestSecurityHandler_GetStatus_BackwardCompatibilityOverrides(t *testing.T) } func TestSecurityHandler_AddWAFExclusion_InvalidExistingJSONStillAdds(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityAudit{})) require.NoError(t, db.Create(&models.SecurityConfig{Name: "default", WAFExclusions: "{"}).Error) @@ -910,7 +876,6 @@ func TestSecurityHandler_AddWAFExclusion_InvalidExistingJSONStillAdds(t *testing } func TestSecurityHandler_ToggleSecurityModule_SnapshotSettingsError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) @@ -935,7 +900,6 @@ func TestSecurityHandler_ToggleSecurityModule_SnapshotSettingsError(t *testing.T } func TestSecurityHandler_ToggleSecurityModule_SnapshotSecurityConfigError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) require.NoError(t, db.Exec("DROP TABLE security_configs").Error) @@ -957,7 +921,6 @@ func TestSecurityHandler_ToggleSecurityModule_SnapshotSecurityConfigError(t *tes } func TestSecurityHandler_SnapshotAndRestoreHelpers(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) @@ -983,7 +946,6 @@ func TestSecurityHandler_SnapshotAndRestoreHelpers(t *testing.T) { } func TestSecurityHandler_DefaultSecurityConfigStateHelpers(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -1043,3 +1005,44 @@ func TestLatestConfigApplyState_Helper(t *testing.T) { require.Equal(t, true, state["available"]) require.Equal(t, "applied", state["status"]) } + +// TestSecurityHandler_CreateDecision_StripsEnrichmentFields verifies that +// clients cannot inject system-populated enrichment fields (Scenario, Country, +// ExpiresAt) via the CreateDecision endpoint. +func TestSecurityHandler_CreateDecision_StripsEnrichmentFields(t *testing.T) { + db := setupTestDB(t) + require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityAudit{})) + + handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) + router := gin.New() + router.Use(func(c *gin.Context) { + c.Set("role", "admin") + c.Next() + }) + router.POST("/security/decisions", handler.CreateDecision) + + payload := map[string]any{ + "ip": "10.0.0.1", + "action": "block", + "details": "test", + "scenario": "injected-scenario", + "country": "XX", + "expires_at": "2099-01-01T00:00:00Z", + } + body, _ := json.Marshal(payload) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/security/decisions", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + + // Verify the stored decision has empty enrichment fields + var stored models.SecurityDecision + require.NoError(t, db.First(&stored).Error) + assert.Empty(t, stored.Scenario, "Scenario should not be settable by client") + assert.Empty(t, stored.Country, "Country should not be settable by client") + assert.Nil(t, stored.ExpiresAt, "ExpiresAt should not be settable by client") + assert.Equal(t, "manual", stored.Source, "Source must be forced to manual") +} diff --git a/backend/internal/api/handlers/security_handler_fixed_test.go b/backend/internal/api/handlers/security_handler_fixed_test.go index 6148e992c..44fe8d0a5 100644 --- a/backend/internal/api/handlers/security_handler_fixed_test.go +++ b/backend/internal/api/handlers/security_handler_fixed_test.go @@ -13,7 +13,6 @@ import ( ) func TestSecurityHandler_GetStatus_Fixed(t *testing.T) { - gin.SetMode(gin.TestMode) tests := []struct { name string diff --git a/backend/internal/api/handlers/security_handler_settings_test.go b/backend/internal/api/handlers/security_handler_settings_test.go index c351daf87..2c61f8bf8 100644 --- a/backend/internal/api/handlers/security_handler_settings_test.go +++ b/backend/internal/api/handlers/security_handler_settings_test.go @@ -21,7 +21,6 @@ import ( // reads WAF, Rate Limit, and CrowdSec enabled states from the settings table, // overriding the static config values. func TestSecurityHandler_GetStatus_RespectsSettingsTable(t *testing.T) { - gin.SetMode(gin.TestMode) tests := []struct { name string @@ -167,7 +166,6 @@ func TestSecurityHandler_GetStatus_RespectsSettingsTable(t *testing.T) { // TestSecurityHandler_GetStatus_WAFModeFromSettings verifies that WAF mode // is properly reflected when enabled via settings. func TestSecurityHandler_GetStatus_WAFModeFromSettings(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{})) @@ -200,7 +198,6 @@ func TestSecurityHandler_GetStatus_WAFModeFromSettings(t *testing.T) { // TestSecurityHandler_GetStatus_RateLimitModeFromSettings verifies that Rate Limit mode // is properly reflected when enabled via settings. func TestSecurityHandler_GetStatus_RateLimitModeFromSettings(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{})) @@ -234,7 +231,6 @@ func TestSecurityHandler_GetStatus_RateLimitModeFromSettings(t *testing.T) { } func TestSecurityHandler_GetStatus_IncludesLatestConfigApplyState(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.CaddyConfig{})) @@ -261,7 +257,6 @@ func TestSecurityHandler_GetStatus_IncludesLatestConfigApplyState(t *testing.T) } func TestSecurityHandler_PatchACL_RequiresAdminWhitelist(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) require.NoError(t, db.Create(&models.SecurityConfig{Name: "default", AdminWhitelist: "192.0.2.1/32"}).Error) @@ -283,7 +278,6 @@ func TestSecurityHandler_PatchACL_RequiresAdminWhitelist(t *testing.T) { } func TestSecurityHandler_PatchACL_AllowsWhitelistedIP(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDBWithMigrations(t) require.NoError(t, db.Create(&models.SecurityConfig{Name: "default", AdminWhitelist: "203.0.113.0/24"}).Error) @@ -315,7 +309,6 @@ func TestSecurityHandler_PatchACL_AllowsWhitelistedIP(t *testing.T) { } func TestSecurityHandler_PatchACL_SetsACLAndCerberusSettings(t *testing.T) { - gin.SetMode(gin.TestMode) dsn := "file:TestSecurityHandler_PatchACL_SetsACLAndCerberusSettings?mode=memory&cache=shared" db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) @@ -352,7 +345,6 @@ func TestSecurityHandler_PatchACL_SetsACLAndCerberusSettings(t *testing.T) { } func TestSecurityHandler_EnsureSecurityConfigEnabled_CreatesWhenMissing(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) @@ -368,7 +360,6 @@ func TestSecurityHandler_EnsureSecurityConfigEnabled_CreatesWhenMissing(t *testi } func TestSecurityHandler_PatchACL_AllowsEmergencyBypass(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) require.NoError(t, db.Create(&models.SecurityConfig{Name: "default", AdminWhitelist: "192.0.2.1/32"}).Error) diff --git a/backend/internal/api/handlers/security_handler_waf_test.go b/backend/internal/api/handlers/security_handler_waf_test.go index 9f338b061..c02a040c5 100644 --- a/backend/internal/api/handlers/security_handler_waf_test.go +++ b/backend/internal/api/handlers/security_handler_waf_test.go @@ -25,7 +25,6 @@ import ( // Tests for GetWAFExclusions handler func TestSecurityHandler_GetWAFExclusions_Empty(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -46,7 +45,6 @@ func TestSecurityHandler_GetWAFExclusions_Empty(t *testing.T) { } func TestSecurityHandler_GetWAFExclusions_WithExclusions(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -77,7 +75,6 @@ func TestSecurityHandler_GetWAFExclusions_WithExclusions(t *testing.T) { } func TestSecurityHandler_GetWAFExclusions_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -104,7 +101,6 @@ func TestSecurityHandler_GetWAFExclusions_InvalidJSON(t *testing.T) { // Tests for AddWAFExclusion handler func TestSecurityHandler_AddWAFExclusion_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityAudit{})) @@ -138,7 +134,6 @@ func TestSecurityHandler_AddWAFExclusion_Success(t *testing.T) { } func TestSecurityHandler_AddWAFExclusion_WithTarget(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityAudit{})) @@ -172,7 +167,6 @@ func TestSecurityHandler_AddWAFExclusion_WithTarget(t *testing.T) { } func TestSecurityHandler_AddWAFExclusion_ToExistingConfig(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityAudit{})) @@ -216,7 +210,6 @@ func TestSecurityHandler_AddWAFExclusion_ToExistingConfig(t *testing.T) { } func TestSecurityHandler_AddWAFExclusion_Duplicate(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityAudit{})) @@ -249,7 +242,6 @@ func TestSecurityHandler_AddWAFExclusion_Duplicate(t *testing.T) { } func TestSecurityHandler_AddWAFExclusion_DuplicateWithDifferentTarget(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityAudit{})) @@ -282,7 +274,6 @@ func TestSecurityHandler_AddWAFExclusion_DuplicateWithDifferentTarget(t *testing } func TestSecurityHandler_AddWAFExclusion_MissingRuleID(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -308,7 +299,6 @@ func TestSecurityHandler_AddWAFExclusion_MissingRuleID(t *testing.T) { } func TestSecurityHandler_AddWAFExclusion_InvalidRuleID(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -335,7 +325,6 @@ func TestSecurityHandler_AddWAFExclusion_InvalidRuleID(t *testing.T) { } func TestSecurityHandler_AddWAFExclusion_NegativeRuleID(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -361,7 +350,6 @@ func TestSecurityHandler_AddWAFExclusion_NegativeRuleID(t *testing.T) { } func TestSecurityHandler_AddWAFExclusion_InvalidPayload(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -383,7 +371,6 @@ func TestSecurityHandler_AddWAFExclusion_InvalidPayload(t *testing.T) { // Tests for DeleteWAFExclusion handler func TestSecurityHandler_DeleteWAFExclusion_Success(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityAudit{})) @@ -423,7 +410,6 @@ func TestSecurityHandler_DeleteWAFExclusion_Success(t *testing.T) { } func TestSecurityHandler_DeleteWAFExclusion_WithTarget(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityAudit{})) @@ -463,7 +449,6 @@ func TestSecurityHandler_DeleteWAFExclusion_WithTarget(t *testing.T) { } func TestSecurityHandler_DeleteWAFExclusion_NotFound(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -488,7 +473,6 @@ func TestSecurityHandler_DeleteWAFExclusion_NotFound(t *testing.T) { } func TestSecurityHandler_DeleteWAFExclusion_NoConfig(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -508,7 +492,6 @@ func TestSecurityHandler_DeleteWAFExclusion_NoConfig(t *testing.T) { } func TestSecurityHandler_DeleteWAFExclusion_InvalidRuleID(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -528,7 +511,6 @@ func TestSecurityHandler_DeleteWAFExclusion_InvalidRuleID(t *testing.T) { } func TestSecurityHandler_DeleteWAFExclusion_ZeroRuleID(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -548,7 +530,6 @@ func TestSecurityHandler_DeleteWAFExclusion_ZeroRuleID(t *testing.T) { } func TestSecurityHandler_DeleteWAFExclusion_NegativeRuleID(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -569,7 +550,6 @@ func TestSecurityHandler_DeleteWAFExclusion_NegativeRuleID(t *testing.T) { // Integration test: Full WAF exclusion workflow func TestSecurityHandler_WAFExclusion_FullWorkflow(t *testing.T) { - gin.SetMode(gin.TestMode) // Create a temporary file-based SQLite database for complete isolation // This avoids all the shared memory locking issues with in-memory databases @@ -673,7 +653,6 @@ func TestSecurityHandler_WAFExclusion_FullWorkflow(t *testing.T) { // Test WAFDisabled field on ProxyHost func TestProxyHost_WAFDisabled_DefaultFalse(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.ProxyHost{})) @@ -693,7 +672,6 @@ func TestProxyHost_WAFDisabled_DefaultFalse(t *testing.T) { } func TestProxyHost_WAFDisabled_SetTrue(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.ProxyHost{})) @@ -715,7 +693,6 @@ func TestProxyHost_WAFDisabled_SetTrue(t *testing.T) { // Test WAFParanoiaLevel field on SecurityConfig func TestSecurityConfig_WAFParanoiaLevel_Default(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -733,7 +710,6 @@ func TestSecurityConfig_WAFParanoiaLevel_Default(t *testing.T) { } func TestSecurityConfig_WAFParanoiaLevel_CustomValue(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -752,7 +728,6 @@ func TestSecurityConfig_WAFParanoiaLevel_CustomValue(t *testing.T) { // Test WAFExclusions field on SecurityConfig func TestSecurityConfig_WAFExclusions_Empty(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) @@ -769,7 +744,6 @@ func TestSecurityConfig_WAFExclusions_Empty(t *testing.T) { } func TestSecurityConfig_WAFExclusions_JSONArray(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) diff --git a/backend/internal/api/handlers/security_headers_handler_test.go b/backend/internal/api/handlers/security_headers_handler_test.go index da30ab3c8..441be0799 100644 --- a/backend/internal/api/handlers/security_headers_handler_test.go +++ b/backend/internal/api/handlers/security_headers_handler_test.go @@ -23,7 +23,6 @@ func setupSecurityHeadersTestRouter(t *testing.T) (*gin.Engine, *gorm.DB) { err = db.AutoMigrate(&models.SecurityHeaderProfile{}, &models.ProxyHost{}) assert.NoError(t, err) - gin.SetMode(gin.TestMode) router := gin.New() handler := NewSecurityHeadersHandler(db, nil) @@ -638,7 +637,6 @@ func TestUpdateProfile_LookupDBError(t *testing.T) { err = db.AutoMigrate(&models.SecurityHeaderProfile{}, &models.ProxyHost{}) assert.NoError(t, err) - gin.SetMode(gin.TestMode) router := gin.New() handler := NewSecurityHeadersHandler(db, nil) @@ -685,7 +683,6 @@ func TestDeleteProfile_LookupDBError(t *testing.T) { err = db.AutoMigrate(&models.SecurityHeaderProfile{}, &models.ProxyHost{}) assert.NoError(t, err) - gin.SetMode(gin.TestMode) router := gin.New() handler := NewSecurityHeadersHandler(db, nil) @@ -716,7 +713,6 @@ func TestDeleteProfile_CountDBError(t *testing.T) { } db.Create(&profile) - gin.SetMode(gin.TestMode) router := gin.New() handler := NewSecurityHeadersHandler(db, nil) @@ -742,7 +738,6 @@ func TestDeleteProfile_DeleteDBError(t *testing.T) { } db.Create(&profile) - gin.SetMode(gin.TestMode) router := gin.New() handler := NewSecurityHeadersHandler(db, nil) @@ -852,7 +847,6 @@ func TestGetProfile_UUID_DBError_NonNotFound(t *testing.T) { err = db.AutoMigrate(&models.SecurityHeaderProfile{}, &models.ProxyHost{}) assert.NoError(t, err) - gin.SetMode(gin.TestMode) router := gin.New() handler := NewSecurityHeadersHandler(db, nil) @@ -902,7 +896,6 @@ func TestUpdateProfile_SaveError(t *testing.T) { db.Create(&profile) profileID := profile.ID - gin.SetMode(gin.TestMode) router := gin.New() handler := NewSecurityHeadersHandler(db, nil) diff --git a/backend/internal/api/handlers/security_notifications_compatibility_test.go b/backend/internal/api/handlers/security_notifications_compatibility_test.go index 39664c0d4..c53e3b97f 100644 --- a/backend/internal/api/handlers/security_notifications_compatibility_test.go +++ b/backend/internal/api/handlers/security_notifications_compatibility_test.go @@ -67,7 +67,6 @@ func TestCompatibilityGET_ORAggregation(t *testing.T) { service := services.NewEnhancedSecurityNotificationService(db) handler := NewSecurityNotificationHandler(service) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/v1/notifications/settings/security", http.NoBody) @@ -104,7 +103,6 @@ func TestCompatibilityGET_AllFalse(t *testing.T) { service := services.NewEnhancedSecurityNotificationService(db) handler := NewSecurityNotificationHandler(service) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/v1/notifications/settings/security", http.NoBody) @@ -145,7 +143,6 @@ func TestCompatibilityGET_DisabledProvidersIgnored(t *testing.T) { service := services.NewEnhancedSecurityNotificationService(db) handler := NewSecurityNotificationHandler(service) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/v1/notifications/settings/security", http.NoBody) @@ -182,7 +179,6 @@ func TestCompatibilityPUT_DeterministicTargetSet(t *testing.T) { "security_rate_limit_enabled": true }`) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) setAdminContext(c) @@ -216,7 +212,6 @@ func TestCompatibilityPUT_CreatesManagedProviderIfNone(t *testing.T) { "webhook_url": "https://example.com/webhook" }`) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) setAdminContext(c) @@ -259,7 +254,6 @@ func TestCompatibilityPUT_Idempotency(t *testing.T) { }`) // First PUT - gin.SetMode(gin.TestMode) w1 := httptest.NewRecorder() c1, _ := gin.CreateTestContext(w1) setAdminContext(c1) @@ -305,7 +299,6 @@ func TestCompatibilityPUT_WebhookMapping(t *testing.T) { "webhook_url": "https://example.com/webhook" }`) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) setAdminContext(c) @@ -336,7 +329,6 @@ func TestCompatibilityPUT_MultipleDestinations422(t *testing.T) { "discord_webhook_url": "https://discord.com/webhook" }`) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) setAdminContext(c) @@ -432,7 +424,6 @@ func TestCompatibilityPUT_MultipleManagedProviders_UpdatesAll(t *testing.T) { "security_rate_limit_enabled": true }`) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) setAdminContext(c) @@ -547,7 +538,6 @@ func TestFeatureFlag_Disabled(t *testing.T) { handler := NewSecurityNotificationHandler(service) // GET should still work via compatibility path - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/v1/notifications/settings/security", http.NoBody) diff --git a/backend/internal/api/handlers/security_notifications_final_blockers_test.go b/backend/internal/api/handlers/security_notifications_final_blockers_test.go index ff924c423..cd5240d13 100644 --- a/backend/internal/api/handlers/security_notifications_final_blockers_test.go +++ b/backend/internal/api/handlers/security_notifications_final_blockers_test.go @@ -31,7 +31,6 @@ func TestFinalBlocker1_DestinationAmbiguous_ZeroManagedProviders(t *testing.T) { service := services.NewEnhancedSecurityNotificationService(db) handler := NewSecurityNotificationHandler(service) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/v1/notifications/settings/security", http.NoBody) @@ -67,7 +66,6 @@ func TestFinalBlocker1_DestinationAmbiguous_OneManagedProvider(t *testing.T) { service := services.NewEnhancedSecurityNotificationService(db) handler := NewSecurityNotificationHandler(service) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/v1/notifications/settings/security", http.NoBody) @@ -112,7 +110,6 @@ func TestFinalBlocker1_DestinationAmbiguous_MultipleManagedProviders(t *testing. service := services.NewEnhancedSecurityNotificationService(db) handler := NewSecurityNotificationHandler(service) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/v1/notifications/settings/security", http.NoBody) @@ -201,7 +198,6 @@ func TestFinalBlocker3_SupportedProviderTypes_WebhookDiscordSlackGotifyOnly(t *t service := services.NewEnhancedSecurityNotificationService(db) handler := NewSecurityNotificationHandler(service) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/v1/notifications/settings/security", http.NoBody) @@ -240,7 +236,6 @@ func TestFinalBlocker3_SupportedProviderTypes_UnsupportedTypesIgnored(t *testing service := services.NewEnhancedSecurityNotificationService(db) handler := NewSecurityNotificationHandler(service) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/v1/notifications/settings/security", http.NoBody) @@ -277,7 +272,6 @@ func TestBlocker2_GETReturnsSecurityFields(t *testing.T) { service := services.NewEnhancedSecurityNotificationService(db) handler := NewSecurityNotificationHandler(service) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/v1/notifications/settings/security", http.NoBody) @@ -320,7 +314,6 @@ func TestBlocker2_GotifyTokenNeverExposed_Legacy(t *testing.T) { service := services.NewEnhancedSecurityNotificationService(db) handler := NewSecurityNotificationHandler(service) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/v1/notifications/settings/security", http.NoBody) diff --git a/backend/internal/api/handlers/security_notifications_patch_coverage_test.go b/backend/internal/api/handlers/security_notifications_patch_coverage_test.go index 4dc267115..4e63c9f68 100644 --- a/backend/internal/api/handlers/security_notifications_patch_coverage_test.go +++ b/backend/internal/api/handlers/security_notifications_patch_coverage_test.go @@ -27,7 +27,6 @@ func TestDeprecatedGetSettings_HeadersSet(t *testing.T) { service := services.NewEnhancedSecurityNotificationService(db) handler := NewSecurityNotificationHandler(service) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/v1/notifications/settings/legacy/security", http.NoBody) @@ -59,7 +58,6 @@ func TestHandleSecurityEvent_InvalidCIDRWarning(t *testing.T) { invalidCIDRs, ) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -98,7 +96,6 @@ func TestHandleSecurityEvent_SeveritySet(t *testing.T) { []string{}, ) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -159,7 +156,6 @@ func TestHandleSecurityEvent_DispatchError(t *testing.T) { []string{}, ) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) diff --git a/backend/internal/api/handlers/security_notifications_single_source_test.go b/backend/internal/api/handlers/security_notifications_single_source_test.go index fbf057296..405161eb1 100644 --- a/backend/internal/api/handlers/security_notifications_single_source_test.go +++ b/backend/internal/api/handlers/security_notifications_single_source_test.go @@ -60,7 +60,6 @@ func TestR2_ProviderSecurityEventsCrowdSecDecisions(t *testing.T) { service := services.NewEnhancedSecurityNotificationService(db) handler := NewSecurityNotificationHandler(service) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/v1/notifications/settings/security", http.NoBody) @@ -103,7 +102,6 @@ func TestR2_ProviderSecurityEventsCrowdSecDecisionsORSemantics(t *testing.T) { service := services.NewEnhancedSecurityNotificationService(db) handler := NewSecurityNotificationHandler(service) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/v1/notifications/settings/security", http.NoBody) @@ -127,7 +125,6 @@ func TestR6_LegacySecuritySettingsWrite410Gone(t *testing.T) { service := services.NewEnhancedSecurityNotificationService(db) handler := NewSecurityNotificationHandler(service) - gin.SetMode(gin.TestMode) // Test canonical endpoint: PUT /api/v1/notifications/settings/security t.Run("CanonicalEndpoint", func(t *testing.T) { @@ -206,7 +203,6 @@ func TestR6_LegacyWrite410GoneNoMutation(t *testing.T) { service := services.NewEnhancedSecurityNotificationService(db) handler := NewSecurityNotificationHandler(service) - gin.SetMode(gin.TestMode) // Attempt PUT to canonical endpoint reqBody := map[string]interface{}{ @@ -241,7 +237,6 @@ func TestProviderCRUD_SecurityEventsIncludeCrowdSec(t *testing.T) { service := services.NewNotificationService(db, nil) handler := NewNotificationProviderHandler(service) - gin.SetMode(gin.TestMode) // Test CREATE t.Run("CreatePersistsCrowdSec", func(t *testing.T) { @@ -329,7 +324,6 @@ func TestR2_CompatibilityGETIncludesCrowdSec(t *testing.T) { service := services.NewEnhancedSecurityNotificationService(db) handler := NewSecurityNotificationHandler(service) - gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/v1/notifications/settings/security", http.NoBody) diff --git a/backend/internal/api/handlers/security_notifications_test.go b/backend/internal/api/handlers/security_notifications_test.go index 8e9f0494f..f401a047d 100644 --- a/backend/internal/api/handlers/security_notifications_test.go +++ b/backend/internal/api/handlers/security_notifications_test.go @@ -21,7 +21,6 @@ import ( // TestHandleSecurityEvent_TimestampZero covers line 146 func TestHandleSecurityEvent_TimestampZero(t *testing.T) { - gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) assert.NoError(t, err) @@ -76,7 +75,6 @@ func (m *mockFailingService) SendViaProviders(ctx context.Context, event models. // TestHandleSecurityEvent_SendViaProvidersError covers lines 163-164 func TestHandleSecurityEvent_SendViaProvidersError(t *testing.T) { - gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) assert.NoError(t, err) diff --git a/backend/internal/api/handlers/security_priority_test.go b/backend/internal/api/handlers/security_priority_test.go index 6b29d0d27..50b05bc77 100644 --- a/backend/internal/api/handlers/security_priority_test.go +++ b/backend/internal/api/handlers/security_priority_test.go @@ -19,7 +19,6 @@ import ( // 2. SecurityConfig DB (middle) // 3. Static config (lowest) func TestSecurityHandler_Priority_SettingsOverSecurityConfig(t *testing.T) { - gin.SetMode(gin.TestMode) tests := []struct { name string @@ -112,7 +111,6 @@ func TestSecurityHandler_Priority_SettingsOverSecurityConfig(t *testing.T) { // TestSecurityHandler_Priority_AllModules verifies priority system works for all security modules func TestSecurityHandler_Priority_AllModules(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) diff --git a/backend/internal/api/handlers/security_ratelimit_test.go b/backend/internal/api/handlers/security_ratelimit_test.go index 8b437409a..f1561761c 100644 --- a/backend/internal/api/handlers/security_ratelimit_test.go +++ b/backend/internal/api/handlers/security_ratelimit_test.go @@ -14,7 +14,6 @@ import ( ) func TestSecurityHandler_GetRateLimitPresets(t *testing.T) { - gin.SetMode(gin.TestMode) cfg := config.SecurityConfig{} handler := NewSecurityHandler(cfg, nil, nil) @@ -49,7 +48,6 @@ func TestSecurityHandler_GetRateLimitPresets(t *testing.T) { } func TestSecurityHandler_GetRateLimitPresets_StandardPreset(t *testing.T) { - gin.SetMode(gin.TestMode) cfg := config.SecurityConfig{} handler := NewSecurityHandler(cfg, nil, nil) @@ -75,7 +73,6 @@ func TestSecurityHandler_GetRateLimitPresets_StandardPreset(t *testing.T) { } func TestSecurityHandler_GetRateLimitPresets_LoginPreset(t *testing.T) { - gin.SetMode(gin.TestMode) cfg := config.SecurityConfig{} handler := NewSecurityHandler(cfg, nil, nil) diff --git a/backend/internal/api/handlers/security_toggles_test.go b/backend/internal/api/handlers/security_toggles_test.go index 929ad3fe9..e167fac4f 100644 --- a/backend/internal/api/handlers/security_toggles_test.go +++ b/backend/internal/api/handlers/security_toggles_test.go @@ -17,7 +17,6 @@ import ( ) func setupToggleTest(t *testing.T) (*SecurityHandler, *gorm.DB) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) @@ -213,7 +212,6 @@ func TestACLEnabledIfIPWhitelisted(t *testing.T) { } func TestSecurityToggles_RollbackSettingWhenApplyFails(t *testing.T) { - gin.SetMode(gin.TestMode) db := OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) require.NoError(t, db.Create(&models.SecurityConfig{Name: "default", Enabled: true}).Error) diff --git a/backend/internal/api/handlers/settings_handler_test.go b/backend/internal/api/handlers/settings_handler_test.go index 708c17580..11b4db2b6 100644 --- a/backend/internal/api/handlers/settings_handler_test.go +++ b/backend/internal/api/handlers/settings_handler_test.go @@ -160,7 +160,6 @@ func newAdminRouter() *gin.Engine { } func TestSettingsHandler_GetSettings(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) // Seed data @@ -183,7 +182,6 @@ func TestSettingsHandler_GetSettings(t *testing.T) { } func TestSettingsHandler_GetSettings_MasksSensitiveValues(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) db.Create(&models.Setting{Key: "smtp_password", Value: "super-secret-password", Category: "smtp", Type: "string"}) @@ -208,7 +206,6 @@ func TestSettingsHandler_GetSettings_MasksSensitiveValues(t *testing.T) { } func TestSettingsHandler_GetSettings_DatabaseError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) // Close the database to force an error @@ -231,7 +228,6 @@ func TestSettingsHandler_GetSettings_DatabaseError(t *testing.T) { } func TestSettingsHandler_UpdateSettings(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) handler := handlers.NewSettingsHandler(db) @@ -274,7 +270,6 @@ func TestSettingsHandler_UpdateSettings(t *testing.T) { } func TestSettingsHandler_UpdateSetting_SyncsAdminWhitelist(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) handler := handlers.NewSettingsHandler(db) @@ -301,7 +296,6 @@ func TestSettingsHandler_UpdateSetting_SyncsAdminWhitelist(t *testing.T) { } func TestSettingsHandler_UpdateSetting_EnablesCerberusWhenACLEnabled(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) handler := handlers.NewSettingsHandler(db) @@ -343,7 +337,6 @@ func TestSettingsHandler_UpdateSetting_EnablesCerberusWhenACLEnabled(t *testing. } func TestSettingsHandler_UpdateSetting_SecurityKeyAppliesConfigSynchronously(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) mgr := &mockCaddyConfigManager{} @@ -367,7 +360,6 @@ func TestSettingsHandler_UpdateSetting_SecurityKeyAppliesConfigSynchronously(t * } func TestSettingsHandler_UpdateSetting_SecurityKeyApplyFailureReturnsError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) mgr := &mockCaddyConfigManager{applyFunc: func(context.Context) error { @@ -393,7 +385,6 @@ func TestSettingsHandler_UpdateSetting_SecurityKeyApplyFailureReturnsError(t *te } func TestSettingsHandler_UpdateSetting_NonAdminForbidden(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) handler := handlers.NewSettingsHandler(db) @@ -416,7 +407,6 @@ func TestSettingsHandler_UpdateSetting_NonAdminForbidden(t *testing.T) { } func TestSettingsHandler_UpdateSetting_InvalidAdminWhitelist(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) handler := handlers.NewSettingsHandler(db) @@ -439,7 +429,6 @@ func TestSettingsHandler_UpdateSetting_InvalidAdminWhitelist(t *testing.T) { } func TestSettingsHandler_UpdateSetting_EmptyValueAccepted(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) handler := handlers.NewSettingsHandler(db) @@ -466,7 +455,6 @@ func TestSettingsHandler_UpdateSetting_EmptyValueAccepted(t *testing.T) { } func TestSettingsHandler_UpdateSetting_MissingKeyRejected(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) handler := handlers.NewSettingsHandler(db) @@ -488,7 +476,6 @@ func TestSettingsHandler_UpdateSetting_MissingKeyRejected(t *testing.T) { } func TestSettingsHandler_UpdateSetting_InvalidKeepaliveIdle(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) handler := handlers.NewSettingsHandler(db) @@ -511,7 +498,6 @@ func TestSettingsHandler_UpdateSetting_InvalidKeepaliveIdle(t *testing.T) { } func TestSettingsHandler_UpdateSetting_ValidKeepaliveCount(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) handler := handlers.NewSettingsHandler(db) @@ -540,7 +526,6 @@ func TestSettingsHandler_UpdateSetting_ValidKeepaliveCount(t *testing.T) { } func TestSettingsHandler_UpdateSetting_SecurityKeyInvalidatesCache(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) mgr := &mockCaddyConfigManager{} @@ -566,7 +551,6 @@ func TestSettingsHandler_UpdateSetting_SecurityKeyInvalidatesCache(t *testing.T) } func TestSettingsHandler_PatchConfig_InvalidAdminWhitelist(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) handler := handlers.NewSettingsHandler(db) @@ -590,7 +574,6 @@ func TestSettingsHandler_PatchConfig_InvalidAdminWhitelist(t *testing.T) { } func TestSettingsHandler_PatchConfig_InvalidKeepaliveCount(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) handler := handlers.NewSettingsHandler(db) @@ -614,7 +597,6 @@ func TestSettingsHandler_PatchConfig_InvalidKeepaliveCount(t *testing.T) { } func TestSettingsHandler_PatchConfig_ValidKeepaliveSettings(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) handler := handlers.NewSettingsHandler(db) @@ -648,7 +630,6 @@ func TestSettingsHandler_PatchConfig_ValidKeepaliveSettings(t *testing.T) { } func TestSettingsHandler_PatchConfig_ReloadFailureReturns500(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) mgr := &mockCaddyConfigManager{applyFunc: func(context.Context) error { @@ -678,7 +659,6 @@ func TestSettingsHandler_PatchConfig_ReloadFailureReturns500(t *testing.T) { } func TestSettingsHandler_PatchConfig_SyncsAdminWhitelist(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) handler := handlers.NewSettingsHandler(db) @@ -710,7 +690,6 @@ func TestSettingsHandler_PatchConfig_SyncsAdminWhitelist(t *testing.T) { } func TestSettingsHandler_PatchConfig_EnablesCerberusWhenACLEnabled(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) handler := handlers.NewSettingsHandler(db) @@ -749,7 +728,6 @@ func TestSettingsHandler_PatchConfig_EnablesCerberusWhenACLEnabled(t *testing.T) } func TestSettingsHandler_UpdateSetting_DatabaseError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) handler := handlers.NewSettingsHandler(db) @@ -779,7 +757,6 @@ func TestSettingsHandler_UpdateSetting_DatabaseError(t *testing.T) { } func TestSettingsHandler_Errors(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) handler := handlers.NewSettingsHandler(db) @@ -830,7 +807,6 @@ func setupSettingsHandlerWithMail(t *testing.T) (*handlers.SettingsHandler, *gor } func TestSettingsHandler_GetSMTPConfig(t *testing.T) { - gin.SetMode(gin.TestMode) handler, db := setupSettingsHandlerWithMail(t) // Seed SMTP config @@ -859,7 +835,6 @@ func TestSettingsHandler_GetSMTPConfig(t *testing.T) { } func TestSettingsHandler_GetSMTPConfig_Empty(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -877,7 +852,6 @@ func TestSettingsHandler_GetSMTPConfig_Empty(t *testing.T) { } func TestSettingsHandler_GetSMTPConfig_DatabaseError(t *testing.T) { - gin.SetMode(gin.TestMode) handler, db := setupSettingsHandlerWithMail(t) sqlDB, _ := db.DB() _ = sqlDB.Close() @@ -893,7 +867,6 @@ func TestSettingsHandler_GetSMTPConfig_DatabaseError(t *testing.T) { } func TestSettingsHandler_GetSMTPConfig_NonAdminForbidden(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := gin.New() @@ -912,7 +885,6 @@ func TestSettingsHandler_GetSMTPConfig_NonAdminForbidden(t *testing.T) { } func TestSettingsHandler_UpdateSMTPConfig_NonAdmin(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -938,7 +910,6 @@ func TestSettingsHandler_UpdateSMTPConfig_NonAdmin(t *testing.T) { } func TestSettingsHandler_UpdateSMTPConfig_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -957,7 +928,6 @@ func TestSettingsHandler_UpdateSMTPConfig_InvalidJSON(t *testing.T) { } func TestSettingsHandler_UpdateSMTPConfig_Success(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -985,7 +955,6 @@ func TestSettingsHandler_UpdateSMTPConfig_Success(t *testing.T) { } func TestSettingsHandler_UpdateSMTPConfig_KeepExistingPassword(t *testing.T) { - gin.SetMode(gin.TestMode) handler, db := setupSettingsHandlerWithMail(t) // Seed existing password @@ -1025,7 +994,6 @@ func TestSettingsHandler_UpdateSMTPConfig_KeepExistingPassword(t *testing.T) { } func TestSettingsHandler_TestSMTPConfig_NonAdmin(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1043,7 +1011,6 @@ func TestSettingsHandler_TestSMTPConfig_NonAdmin(t *testing.T) { } func TestSettingsHandler_TestSMTPConfig_NotConfigured(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1064,7 +1031,6 @@ func TestSettingsHandler_TestSMTPConfig_NotConfigured(t *testing.T) { } func TestSettingsHandler_TestSMTPConfig_Success(t *testing.T) { - gin.SetMode(gin.TestMode) handler, db := setupSettingsHandlerWithMail(t) host, port := startTestSMTPServer(t) @@ -1093,7 +1059,6 @@ func TestSettingsHandler_TestSMTPConfig_Success(t *testing.T) { } func TestSettingsHandler_SendTestEmail_NonAdmin(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1114,7 +1079,6 @@ func TestSettingsHandler_SendTestEmail_NonAdmin(t *testing.T) { } func TestSettingsHandler_SendTestEmail_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1133,7 +1097,6 @@ func TestSettingsHandler_SendTestEmail_InvalidJSON(t *testing.T) { } func TestSettingsHandler_SendTestEmail_NotConfigured(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1157,7 +1120,6 @@ func TestSettingsHandler_SendTestEmail_NotConfigured(t *testing.T) { } func TestSettingsHandler_SendTestEmail_Success(t *testing.T) { - gin.SetMode(gin.TestMode) handler, db := setupSettingsHandlerWithMail(t) host, port := startTestSMTPServer(t) @@ -1199,7 +1161,6 @@ func TestMaskPassword(t *testing.T) { // ============= URL Testing Tests ============= func TestSettingsHandler_ValidatePublicURL_NonAdmin(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1220,7 +1181,6 @@ func TestSettingsHandler_ValidatePublicURL_NonAdmin(t *testing.T) { } func TestSettingsHandler_ValidatePublicURL_InvalidFormat(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1257,7 +1217,6 @@ func TestSettingsHandler_ValidatePublicURL_InvalidFormat(t *testing.T) { } func TestSettingsHandler_ValidatePublicURL_Success(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1297,7 +1256,6 @@ func TestSettingsHandler_ValidatePublicURL_Success(t *testing.T) { } func TestSettingsHandler_TestPublicURL_NonAdmin(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1318,7 +1276,6 @@ func TestSettingsHandler_TestPublicURL_NonAdmin(t *testing.T) { } func TestSettingsHandler_TestPublicURL_NoRole(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := gin.New() @@ -1336,7 +1293,6 @@ func TestSettingsHandler_TestPublicURL_NoRole(t *testing.T) { } func TestSettingsHandler_TestPublicURL_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1355,7 +1311,6 @@ func TestSettingsHandler_TestPublicURL_InvalidJSON(t *testing.T) { } func TestSettingsHandler_TestPublicURL_InvalidURL(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1380,7 +1335,6 @@ func TestSettingsHandler_TestPublicURL_InvalidURL(t *testing.T) { } func TestSettingsHandler_TestPublicURL_PrivateIPBlocked(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1430,7 +1384,6 @@ func contains(s, substr string) bool { } func TestSettingsHandler_TestPublicURL_Success(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) // NOTE: Using a real public URL instead of httptest.NewServer() because @@ -1464,7 +1417,6 @@ func TestSettingsHandler_TestPublicURL_Success(t *testing.T) { } func TestSettingsHandler_TestPublicURL_DNSFailure(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1493,7 +1445,6 @@ func TestSettingsHandler_TestPublicURL_DNSFailure(t *testing.T) { } func TestSettingsHandler_TestPublicURL_ConnectivityError(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1584,7 +1535,6 @@ func TestSettingsHandler_TestPublicURL_SSRFProtection(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1619,7 +1569,6 @@ func TestSettingsHandler_TestPublicURL_SSRFProtection(t *testing.T) { } func TestSettingsHandler_TestPublicURL_EmbeddedCredentials(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1647,7 +1596,6 @@ func TestSettingsHandler_TestPublicURL_EmbeddedCredentials(t *testing.T) { } func TestSettingsHandler_TestPublicURL_EmptyURL(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1679,7 +1627,6 @@ func TestSettingsHandler_TestPublicURL_EmptyURL(t *testing.T) { } func TestSettingsHandler_TestPublicURL_InvalidScheme(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1719,7 +1666,6 @@ func TestSettingsHandler_TestPublicURL_InvalidScheme(t *testing.T) { } func TestSettingsHandler_ValidatePublicURL_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1738,7 +1684,6 @@ func TestSettingsHandler_ValidatePublicURL_InvalidJSON(t *testing.T) { } func TestSettingsHandler_ValidatePublicURL_URLWithWarning(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1765,7 +1710,6 @@ func TestSettingsHandler_ValidatePublicURL_URLWithWarning(t *testing.T) { } func TestSettingsHandler_UpdateSMTPConfig_DatabaseError(t *testing.T) { - gin.SetMode(gin.TestMode) handler, db := setupSettingsHandlerWithMail(t) // Close the database to force an error @@ -1798,7 +1742,6 @@ func TestSettingsHandler_UpdateSMTPConfig_DatabaseError(t *testing.T) { } func TestSettingsHandler_TestPublicURL_IPv6LocalhostBlocked(t *testing.T) { - gin.SetMode(gin.TestMode) handler, _ := setupSettingsHandlerWithMail(t) router := newAdminRouter() @@ -1829,7 +1772,6 @@ func TestSettingsHandler_TestPublicURL_IPv6LocalhostBlocked(t *testing.T) { // the tag is present. Re-adding the tag would silently regress the CrowdSec enable // flow (which sends value="" to clear the setting). func TestUpdateSetting_EmptyValueIsAccepted(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) handler := handlers.NewSettingsHandler(db) @@ -1853,7 +1795,6 @@ func TestUpdateSetting_EmptyValueIsAccepted(t *testing.T) { // from Value and not accidentally also from Key. A request with no "key" field must // still return 400. func TestUpdateSetting_MissingKeyRejected(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t) handler := handlers.NewSettingsHandler(db) diff --git a/backend/internal/api/handlers/system_handler_test.go b/backend/internal/api/handlers/system_handler_test.go index 4c8c6b173..3e873ecfb 100644 --- a/backend/internal/api/handlers/system_handler_test.go +++ b/backend/internal/api/handlers/system_handler_test.go @@ -44,7 +44,6 @@ func TestGetClientIPHeadersAndRemoteAddr(t *testing.T) { } func TestGetMyIPHandler(t *testing.T) { - gin.SetMode(gin.TestMode) r := gin.New() handler := NewSystemHandler() r.GET("/myip", handler.GetMyIP) diff --git a/backend/internal/api/handlers/system_permissions_handler_test.go b/backend/internal/api/handlers/system_permissions_handler_test.go index 5a8f4e2a9..843a6dd1e 100644 --- a/backend/internal/api/handlers/system_permissions_handler_test.go +++ b/backend/internal/api/handlers/system_permissions_handler_test.go @@ -47,7 +47,6 @@ func (stubPermissionChecker) Check(path, required string) util.PermissionCheck { } func TestSystemPermissionsHandler_GetPermissions_Admin(t *testing.T) { - gin.SetMode(gin.TestMode) cfg := config.Config{ DatabasePath: "/app/data/charon.db", @@ -81,7 +80,6 @@ func TestSystemPermissionsHandler_GetPermissions_Admin(t *testing.T) { } func TestSystemPermissionsHandler_GetPermissions_NonAdmin(t *testing.T) { - gin.SetMode(gin.TestMode) cfg := config.Config{} h := NewSystemPermissionsHandler(cfg, nil, stubPermissionChecker{}) @@ -105,7 +103,6 @@ func TestSystemPermissionsHandler_RepairPermissions_NonRoot(t *testing.T) { t.Skip("test requires non-root execution") } - gin.SetMode(gin.TestMode) cfg := config.Config{SingleContainer: true} h := NewSystemPermissionsHandler(cfg, nil, stubPermissionChecker{}) @@ -213,7 +210,6 @@ func TestSystemPermissionsHandler_NewDefaultsCheckerToOSChecker(t *testing.T) { } func TestSystemPermissionsHandler_RepairPermissions_DisabledWhenNotSingleContainer(t *testing.T) { - gin.SetMode(gin.TestMode) h := NewSystemPermissionsHandler(config.Config{SingleContainer: false}, nil, stubPermissionChecker{}) @@ -236,7 +232,6 @@ func TestSystemPermissionsHandler_RepairPermissions_InvalidJSON(t *testing.T) { t.Skip("test requires root execution") } - gin.SetMode(gin.TestMode) root := t.TempDir() dataDir := filepath.Join(root, "data") @@ -269,7 +264,6 @@ func TestSystemPermissionsHandler_RepairPermissions_Success(t *testing.T) { t.Skip("test requires root execution") } - gin.SetMode(gin.TestMode) root := t.TempDir() dataDir := filepath.Join(root, "data") @@ -310,7 +304,6 @@ func TestSystemPermissionsHandler_RepairPermissions_Success(t *testing.T) { } func TestSystemPermissionsHandler_RepairPermissions_NonAdmin(t *testing.T) { - gin.SetMode(gin.TestMode) h := NewSystemPermissionsHandler(config.Config{SingleContainer: true}, nil, stubPermissionChecker{}) @@ -330,7 +323,6 @@ func TestSystemPermissionsHandler_RepairPermissions_InvalidJSONWhenRoot(t *testi t.Skip("test requires root execution") } - gin.SetMode(gin.TestMode) root := t.TempDir() dataDir := filepath.Join(root, "data") require.NoError(t, os.MkdirAll(dataDir, 0o750)) @@ -395,7 +387,6 @@ func TestSystemPermissionsHandler_IsWithinAllowlist_AllRelErrorsReturnFalse(t *t } func TestSystemPermissionsHandler_LogAudit_PersistsAuditWithUserID(t *testing.T) { - gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) require.NoError(t, err) @@ -416,7 +407,6 @@ func TestSystemPermissionsHandler_LogAudit_PersistsAuditWithUserID(t *testing.T) } func TestSystemPermissionsHandler_LogAudit_PersistsAuditWithUnknownActor(t *testing.T) { - gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) require.NoError(t, err) @@ -536,7 +526,6 @@ func TestSystemPermissionsHandler_RepairPermissions_InvalidRequestBody_Root(t *t t.Skip("test requires root execution") } - gin.SetMode(gin.TestMode) tmp := t.TempDir() dataDir := filepath.Join(tmp, "data") diff --git a/backend/internal/api/handlers/system_permissions_wave6_test.go b/backend/internal/api/handlers/system_permissions_wave6_test.go index ad2d7e631..09d34c939 100644 --- a/backend/internal/api/handlers/system_permissions_wave6_test.go +++ b/backend/internal/api/handlers/system_permissions_wave6_test.go @@ -28,7 +28,6 @@ func TestSystemPermissionsWave6_RepairPermissions_NonRootBranchViaSeteuid(t *tes require.NoError(t, restoreErr) }() - gin.SetMode(gin.TestMode) root := t.TempDir() dataDir := filepath.Join(root, "data") diff --git a/backend/internal/api/handlers/testmain_test.go b/backend/internal/api/handlers/testmain_test.go new file mode 100644 index 000000000..2e26b3db3 --- /dev/null +++ b/backend/internal/api/handlers/testmain_test.go @@ -0,0 +1,13 @@ +package handlers + +import ( + "os" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestMain(m *testing.M) { + gin.SetMode(gin.TestMode) + os.Exit(m.Run()) +} diff --git a/backend/internal/api/handlers/update_handler_test.go b/backend/internal/api/handlers/update_handler_test.go index 52c5693c7..3e70837dc 100644 --- a/backend/internal/api/handlers/update_handler_test.go +++ b/backend/internal/api/handlers/update_handler_test.go @@ -33,7 +33,6 @@ func TestUpdateHandler_Check(t *testing.T) { h := NewUpdateHandler(svc) // Setup Router - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/api/v1/update", h.Check) diff --git a/backend/internal/api/handlers/uptime_monitor_initial_state_test.go b/backend/internal/api/handlers/uptime_monitor_initial_state_test.go index 61ab01bca..f0025bab5 100644 --- a/backend/internal/api/handlers/uptime_monitor_initial_state_test.go +++ b/backend/internal/api/handlers/uptime_monitor_initial_state_test.go @@ -19,7 +19,6 @@ import ( // Verifies that newly created monitors start in "pending" state, not "down" func TestUptimeMonitorInitialStatePending(t *testing.T) { t.Parallel() - gin.SetMode(gin.TestMode) db := setupTestDB(t) // Migrate UptimeMonitor model diff --git a/backend/internal/api/handlers/user_handler_coverage_test.go b/backend/internal/api/handlers/user_handler_coverage_test.go index db0133a80..f16c5395b 100644 --- a/backend/internal/api/handlers/user_handler_coverage_test.go +++ b/backend/internal/api/handlers/user_handler_coverage_test.go @@ -21,7 +21,6 @@ func setupUserCoverageDB(t *testing.T) *gorm.DB { } func TestUserHandler_GetSetupStatus_Error(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUserCoverageDB(t) h := NewUserHandler(db, nil) @@ -38,7 +37,6 @@ func TestUserHandler_GetSetupStatus_Error(t *testing.T) { } func TestUserHandler_Setup_CheckStatusError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUserCoverageDB(t) h := NewUserHandler(db, nil) @@ -55,7 +53,6 @@ func TestUserHandler_Setup_CheckStatusError(t *testing.T) { } func TestUserHandler_Setup_AlreadyCompleted(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUserCoverageDB(t) h := NewUserHandler(db, nil) @@ -74,7 +71,6 @@ func TestUserHandler_Setup_AlreadyCompleted(t *testing.T) { } func TestUserHandler_Setup_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUserCoverageDB(t) h := NewUserHandler(db, nil) @@ -89,7 +85,6 @@ func TestUserHandler_Setup_InvalidJSON(t *testing.T) { } func TestUserHandler_RegenerateAPIKey_Unauthorized(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUserCoverageDB(t) h := NewUserHandler(db, nil) @@ -103,7 +98,6 @@ func TestUserHandler_RegenerateAPIKey_Unauthorized(t *testing.T) { } func TestUserHandler_RegenerateAPIKey_DBError(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUserCoverageDB(t) h := NewUserHandler(db, nil) @@ -121,7 +115,6 @@ func TestUserHandler_RegenerateAPIKey_DBError(t *testing.T) { } func TestUserHandler_GetProfile_Unauthorized(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUserCoverageDB(t) h := NewUserHandler(db, nil) @@ -135,7 +128,6 @@ func TestUserHandler_GetProfile_Unauthorized(t *testing.T) { } func TestUserHandler_GetProfile_NotFound(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUserCoverageDB(t) h := NewUserHandler(db, nil) @@ -150,7 +142,6 @@ func TestUserHandler_GetProfile_NotFound(t *testing.T) { } func TestUserHandler_UpdateProfile_Unauthorized(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUserCoverageDB(t) h := NewUserHandler(db, nil) @@ -164,7 +155,6 @@ func TestUserHandler_UpdateProfile_Unauthorized(t *testing.T) { } func TestUserHandler_UpdateProfile_InvalidJSON(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUserCoverageDB(t) h := NewUserHandler(db, nil) @@ -180,7 +170,6 @@ func TestUserHandler_UpdateProfile_InvalidJSON(t *testing.T) { } func TestUserHandler_UpdateProfile_UserNotFound(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUserCoverageDB(t) h := NewUserHandler(db, nil) @@ -201,7 +190,6 @@ func TestUserHandler_UpdateProfile_UserNotFound(t *testing.T) { } func TestUserHandler_UpdateProfile_EmailConflict(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUserCoverageDB(t) h := NewUserHandler(db, nil) @@ -234,7 +222,6 @@ func TestUserHandler_UpdateProfile_EmailConflict(t *testing.T) { } func TestUserHandler_UpdateProfile_EmailChangeNoPassword(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUserCoverageDB(t) h := NewUserHandler(db, nil) @@ -261,7 +248,6 @@ func TestUserHandler_UpdateProfile_EmailChangeNoPassword(t *testing.T) { } func TestUserHandler_UpdateProfile_WrongPassword(t *testing.T) { - gin.SetMode(gin.TestMode) db := setupUserCoverageDB(t) h := NewUserHandler(db, nil) diff --git a/backend/internal/api/handlers/user_handler_test.go b/backend/internal/api/handlers/user_handler_test.go index ab2dee9f0..edf146ef4 100644 --- a/backend/internal/api/handlers/user_handler_test.go +++ b/backend/internal/api/handlers/user_handler_test.go @@ -76,7 +76,6 @@ func TestUserHandler_logUserAudit_NoOpBranches(t *testing.T) { func TestUserHandler_GetSetupStatus(t *testing.T) { handler, db := setupUserHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/setup", handler.GetSetupStatus) @@ -97,7 +96,6 @@ func TestUserHandler_GetSetupStatus(t *testing.T) { func TestUserHandler_Setup(t *testing.T) { handler, _ := setupUserHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() r.POST("/setup", handler.Setup) @@ -133,7 +131,6 @@ func TestUserHandler_Setup(t *testing.T) { func TestUserHandler_Setup_OneWayInvariant_ReentryRejectedAndSingleUser(t *testing.T) { handler, db := setupUserHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() r.POST("/setup", handler.Setup) @@ -170,7 +167,6 @@ func TestUserHandler_Setup_OneWayInvariant_ReentryRejectedAndSingleUser(t *testi func TestUserHandler_Setup_ConcurrentAttemptInvariant(t *testing.T) { handler, db := setupUserHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() r.POST("/setup", handler.Setup) @@ -230,7 +226,6 @@ func TestUserHandler_Setup_ConcurrentAttemptInvariant(t *testing.T) { func TestUserHandler_Setup_ResponseSecretEchoContract(t *testing.T) { handler, _ := setupUserHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() r.POST("/setup", handler.Setup) @@ -279,7 +274,6 @@ func TestUserHandler_GetProfile_SecretEchoContract(t *testing.T) { } require.NoError(t, db.Create(user).Error) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("userID", user.ID) @@ -321,7 +315,6 @@ func TestUserHandler_ListUsers_SecretEchoContract(t *testing.T) { } require.NoError(t, db.Create(user).Error) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -367,7 +360,6 @@ func TestUserHandler_RegenerateAPIKey(t *testing.T) { user := &models.User{Email: "api@example.com"} db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("userID", user.ID) @@ -402,7 +394,6 @@ func TestUserHandler_GetProfile(t *testing.T) { } db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("userID", user.ID) @@ -425,7 +416,6 @@ func TestUserHandler_GetProfile(t *testing.T) { func TestUserHandler_RegisterRoutes(t *testing.T) { handler, _ := setupUserHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() api := r.Group("/api") handler.RegisterRoutes(api) @@ -451,7 +441,6 @@ func TestUserHandler_RegisterRoutes(t *testing.T) { func TestUserHandler_Errors(t *testing.T) { handler, db := setupUserHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() // Middleware to simulate missing userID @@ -518,7 +507,6 @@ func TestUserHandler_UpdateProfile(t *testing.T) { _ = user.SetPassword("password123") db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("userID", user.ID) @@ -624,7 +612,6 @@ func TestUserHandler_UpdateProfile(t *testing.T) { func TestUserHandler_UpdateProfile_Errors(t *testing.T) { handler, _ := setupUserHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() // 1. Unauthorized (no userID) @@ -668,7 +655,6 @@ func setupUserHandlerWithProxyHosts(t *testing.T) (*UserHandler, *gorm.DB) { func TestUserHandler_ListUsers_NonAdmin(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "user") @@ -692,7 +678,6 @@ func TestUserHandler_ListUsers_Admin(t *testing.T) { db.Create(user1) db.Create(user2) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -713,7 +698,6 @@ func TestUserHandler_ListUsers_Admin(t *testing.T) { func TestUserHandler_CreateUser_NonAdmin(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "user") @@ -737,7 +721,6 @@ func TestUserHandler_CreateUser_NonAdmin(t *testing.T) { func TestUserHandler_CreateUser_Admin(t *testing.T) { handler, db := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -767,7 +750,6 @@ func TestUserHandler_CreateUser_Admin(t *testing.T) { func TestUserHandler_CreateUser_InvalidJSON(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -789,7 +771,6 @@ func TestUserHandler_CreateUser_DuplicateEmail(t *testing.T) { existing := &models.User{UUID: uuid.NewString(), Email: "existing@example.com", Name: "Existing"} db.Create(existing) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -817,7 +798,6 @@ func TestUserHandler_CreateUser_WithPermittedHosts(t *testing.T) { host := &models.ProxyHost{Name: "Host 1", DomainNames: "host1.example.com", Enabled: true} db.Create(host) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -843,7 +823,6 @@ func TestUserHandler_CreateUser_WithPermittedHosts(t *testing.T) { func TestUserHandler_GetUser_NonAdmin(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "user") @@ -860,7 +839,6 @@ func TestUserHandler_GetUser_NonAdmin(t *testing.T) { func TestUserHandler_GetUser_InvalidID(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -877,7 +855,6 @@ func TestUserHandler_GetUser_InvalidID(t *testing.T) { func TestUserHandler_GetUser_NotFound(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -898,7 +875,6 @@ func TestUserHandler_GetUser_Success(t *testing.T) { user := &models.User{UUID: uuid.NewString(), Email: "getuser@example.com", Name: "Get User"} db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -920,7 +896,6 @@ func TestUserHandler_UpdateUser_NonAdmin(t *testing.T) { target := &models.User{UUID: uuid.NewString(), Email: "target@example.com", Name: "Target", APIKey: uuid.NewString(), Role: models.RoleUser} db.Create(target) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "user") @@ -941,7 +916,6 @@ func TestUserHandler_UpdateUser_NonAdmin(t *testing.T) { func TestUserHandler_UpdateUser_InvalidID(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -967,7 +941,6 @@ func TestUserHandler_UpdateUser_InvalidJSON(t *testing.T) { user := &models.User{UUID: uuid.NewString(), Email: "toupdate@example.com", Name: "To Update", APIKey: uuid.NewString()} db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -986,7 +959,6 @@ func TestUserHandler_UpdateUser_InvalidJSON(t *testing.T) { func TestUserHandler_UpdateUser_NotFound(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -1011,7 +983,6 @@ func TestUserHandler_UpdateUser_Success(t *testing.T) { user := &models.User{UUID: uuid.NewString(), Email: "update@example.com", Name: "Original", Role: models.RoleUser} db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -1048,7 +1019,6 @@ func TestUserHandler_UpdateUser_PasswordReset(t *testing.T) { user.LockedUntil = &lockUntil db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -1078,7 +1048,6 @@ func TestUserHandler_UpdateUser_PasswordReset(t *testing.T) { func TestUserHandler_DeleteUser_NonAdmin(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "user") @@ -1095,7 +1064,6 @@ func TestUserHandler_DeleteUser_NonAdmin(t *testing.T) { func TestUserHandler_DeleteUser_InvalidID(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -1112,7 +1080,6 @@ func TestUserHandler_DeleteUser_InvalidID(t *testing.T) { func TestUserHandler_DeleteUser_NotFound(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -1134,7 +1101,6 @@ func TestUserHandler_DeleteUser_Success(t *testing.T) { user := &models.User{UUID: uuid.NewString(), Email: "delete@example.com", Name: "Delete Me"} db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -1161,7 +1127,6 @@ func TestUserHandler_DeleteUser_CannotDeleteSelf(t *testing.T) { user := &models.User{UUID: uuid.NewString(), Email: "self@example.com", Name: "Self"} db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -1179,7 +1144,6 @@ func TestUserHandler_DeleteUser_CannotDeleteSelf(t *testing.T) { func TestUserHandler_UpdateUserPermissions_NonAdmin(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "user") @@ -1199,7 +1163,6 @@ func TestUserHandler_UpdateUserPermissions_NonAdmin(t *testing.T) { func TestUserHandler_UpdateUserPermissions_InvalidID(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -1231,7 +1194,6 @@ func TestUserHandler_UpdateUserPermissions_InvalidJSON(t *testing.T) { } db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -1249,7 +1211,6 @@ func TestUserHandler_UpdateUserPermissions_InvalidJSON(t *testing.T) { func TestUserHandler_UpdateUserPermissions_NotFound(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -1281,7 +1242,6 @@ func TestUserHandler_UpdateUserPermissions_Success(t *testing.T) { } db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -1304,7 +1264,6 @@ func TestUserHandler_UpdateUserPermissions_Success(t *testing.T) { func TestUserHandler_ValidateInvite_MissingToken(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/invite/validate", handler.ValidateInvite) @@ -1317,7 +1276,6 @@ func TestUserHandler_ValidateInvite_MissingToken(t *testing.T) { func TestUserHandler_ValidateInvite_InvalidToken(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/invite/validate", handler.ValidateInvite) @@ -1342,7 +1300,6 @@ func TestUserHandler_ValidateInvite_ExpiredToken(t *testing.T) { } db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/invite/validate", handler.ValidateInvite) @@ -1367,7 +1324,6 @@ func TestUserHandler_ValidateInvite_AlreadyAccepted(t *testing.T) { } db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/invite/validate", handler.ValidateInvite) @@ -1392,7 +1348,6 @@ func TestUserHandler_ValidateInvite_Success(t *testing.T) { } db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.GET("/invite/validate", handler.ValidateInvite) @@ -1409,7 +1364,6 @@ func TestUserHandler_ValidateInvite_Success(t *testing.T) { func TestUserHandler_AcceptInvite_InvalidJSON(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.POST("/invite/accept", handler.AcceptInvite) @@ -1423,7 +1377,6 @@ func TestUserHandler_AcceptInvite_InvalidJSON(t *testing.T) { func TestUserHandler_AcceptInvite_InvalidToken(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.POST("/invite/accept", handler.AcceptInvite) @@ -1455,7 +1408,6 @@ func TestUserHandler_AcceptInvite_Success(t *testing.T) { } db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.POST("/invite/accept", handler.AcceptInvite) @@ -1498,7 +1450,6 @@ func TestGenerateSecureToken(t *testing.T) { func TestUserHandler_InviteUser_NonAdmin(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "user") @@ -1519,7 +1470,6 @@ func TestUserHandler_InviteUser_NonAdmin(t *testing.T) { func TestUserHandler_InviteUser_InvalidJSON(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -1547,7 +1497,6 @@ func TestUserHandler_InviteUser_DuplicateEmail(t *testing.T) { } db.Create(existingUser) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -1578,7 +1527,6 @@ func TestUserHandler_InviteUser_Success(t *testing.T) { } db.Create(admin) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -1639,7 +1587,6 @@ func TestUserHandler_InviteUser_WithPermittedHosts(t *testing.T) { } db.Create(host) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -1696,7 +1643,6 @@ func TestUserHandler_InviteUser_WithSMTPConfigured(t *testing.T) { // Reinitialize mail service to pick up new settings handler.MailService = services.NewMailService(db) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -1757,7 +1703,6 @@ func TestUserHandler_InviteUser_WithSMTPAndConfiguredPublicURL_IncludesInviteURL handler.MailService = services.NewMailService(db) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -1810,7 +1755,6 @@ func TestUserHandler_InviteUser_WithSMTPAndMalformedPublicURL_DoesNotExposeInvit handler.MailService = services.NewMailService(db) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -1866,7 +1810,6 @@ func TestUserHandler_InviteUser_WithSMTPConfigured_DefaultAppName(t *testing.T) // Reinitialize mail service to pick up new settings handler.MailService = services.NewMailService(db) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -1917,7 +1860,6 @@ func TestUserHandler_AcceptInvite_ExpiredToken(t *testing.T) { } db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.POST("/invite/accept", handler.AcceptInvite) @@ -1949,7 +1891,6 @@ func TestUserHandler_AcceptInvite_AlreadyAccepted(t *testing.T) { } db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.POST("/invite/accept", handler.AcceptInvite) @@ -1972,7 +1913,6 @@ func TestUserHandler_AcceptInvite_AlreadyAccepted(t *testing.T) { // PreviewInviteURL Tests func TestUserHandler_PreviewInviteURL_NonAdmin(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "user") @@ -1993,7 +1933,6 @@ func TestUserHandler_PreviewInviteURL_NonAdmin(t *testing.T) { func TestUserHandler_PreviewInviteURL_InvalidJSON(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2011,7 +1950,6 @@ func TestUserHandler_PreviewInviteURL_InvalidJSON(t *testing.T) { func TestUserHandler_PreviewInviteURL_Success_Unconfigured(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2052,7 +1990,6 @@ func TestUserHandler_PreviewInviteURL_Success_Configured(t *testing.T) { } db.Create(publicURLSetting) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2145,7 +2082,6 @@ func TestUserHandler_UpdateUser_EmailConflict(t *testing.T) { db.Create(user1) db.Create(user2) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2172,7 +2108,6 @@ func TestUserHandler_UpdateUser_EmailConflict(t *testing.T) { func TestUserHandler_CreateUser_EmailNormalization(t *testing.T) { handler, db := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2212,7 +2147,6 @@ func TestUserHandler_InviteUser_EmailNormalization(t *testing.T) { } db.Create(admin) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2241,7 +2175,6 @@ func TestUserHandler_InviteUser_EmailNormalization(t *testing.T) { func TestUserHandler_CreateUser_DefaultPermissionMode(t *testing.T) { handler, db := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2281,7 +2214,6 @@ func TestUserHandler_InviteUser_DefaultPermissionMode(t *testing.T) { } db.Create(admin) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2310,7 +2242,6 @@ func TestUserHandler_InviteUser_DefaultPermissionMode(t *testing.T) { func TestUserHandler_CreateUser_DefaultRole(t *testing.T) { handler, db := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2350,7 +2281,6 @@ func TestUserHandler_InviteUser_DefaultRole(t *testing.T) { } db.Create(admin) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2382,7 +2312,6 @@ func TestUserHandler_InviteUser_DefaultRole(t *testing.T) { // This prevents host header injection attacks (CodeQL go/email-injection remediation). func TestUserHandler_PreviewInviteURL_Unconfigured_DoesNotUseRequestHost(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2417,7 +2346,6 @@ func TestUserHandler_PreviewInviteURL_Unconfigured_DoesNotUseRequestHost(t *test func TestUserHandler_CreateUser_EmptyPermittedHosts(t *testing.T) { handler, db := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2450,7 +2378,6 @@ func TestUserHandler_CreateUser_EmptyPermittedHosts(t *testing.T) { func TestUserHandler_CreateUser_NonExistentPermittedHosts(t *testing.T) { handler, db := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2484,7 +2411,6 @@ func TestUserHandler_CreateUser_NonExistentPermittedHosts(t *testing.T) { func TestResendInvite_NonAdmin(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "user") @@ -2502,7 +2428,6 @@ func TestResendInvite_NonAdmin(t *testing.T) { func TestResendInvite_InvalidID(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2520,7 +2445,6 @@ func TestResendInvite_InvalidID(t *testing.T) { func TestResendInvite_UserNotFound(t *testing.T) { handler, _ := setupUserHandlerWithProxyHosts(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2550,7 +2474,6 @@ func TestResendInvite_UserNotPending(t *testing.T) { } db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2583,7 +2506,6 @@ func TestResendInvite_Success(t *testing.T) { } db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2628,7 +2550,6 @@ func TestResendInvite_WithExpiredInvite(t *testing.T) { } db.Create(user) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2722,7 +2643,6 @@ func TestRedactInviteURL(t *testing.T) { // --- Passthrough rejection tests --- func setupPassthroughRouter(handler *UserHandler) *gin.Engine { - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", string(models.RolePassthrough)) @@ -2777,7 +2697,6 @@ func TestUserHandler_UpdateProfile_PassthroughRejected(t *testing.T) { func TestUserHandler_CreateUser_InvalidRole(t *testing.T) { handler, _ := setupUserHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2803,7 +2722,6 @@ func TestUserHandler_CreateUser_InvalidRole(t *testing.T) { func TestUserHandler_InviteUser_InvalidRole(t *testing.T) { handler, _ := setupUserHandler(t) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2833,7 +2751,6 @@ func TestUserHandler_UpdateUser_MissingUserID(t *testing.T) { user := models.User{UUID: uuid.NewString(), APIKey: uuid.NewString(), Email: "target@example.com", Role: models.RoleUser, Enabled: true} require.NoError(t, db.Create(&user).Error) - gin.SetMode(gin.TestMode) r := gin.New() // No userID set in context r.Use(func(c *gin.Context) { @@ -2858,7 +2775,6 @@ func TestUserHandler_UpdateUser_InvalidSessionType(t *testing.T) { user := models.User{UUID: uuid.NewString(), APIKey: uuid.NewString(), Email: "target2@example.com", Role: models.RoleUser, Enabled: true} require.NoError(t, db.Create(&user).Error) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2885,7 +2801,6 @@ func TestUserHandler_UpdateUser_NonAdminSelfRoleChange(t *testing.T) { user := models.User{UUID: uuid.NewString(), APIKey: uuid.NewString(), Email: "self@example.com", Role: models.RoleUser, Enabled: true} require.NoError(t, db.Create(&user).Error) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "user") // non-admin @@ -2912,7 +2827,6 @@ func TestUserHandler_UpdateUser_InvalidRole(t *testing.T) { target := models.User{UUID: uuid.NewString(), APIKey: uuid.NewString(), Email: "target3@example.com", Role: models.RoleUser, Enabled: true} require.NoError(t, db.Create(&target).Error) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2939,7 +2853,6 @@ func TestUserHandler_UpdateUser_SelfDemotion(t *testing.T) { admin := models.User{UUID: uuid.NewString(), APIKey: uuid.NewString(), Email: "admin@self.example.com", Role: models.RoleAdmin, Enabled: true} require.NoError(t, db.Create(&admin).Error) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2966,7 +2879,6 @@ func TestUserHandler_UpdateUser_SelfDisable(t *testing.T) { disabled := false - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -2994,7 +2906,6 @@ func TestUserHandler_UpdateUser_LastAdminDemotion(t *testing.T) { target := models.User{UUID: uuid.NewString(), APIKey: uuid.NewString(), Email: "last-admin@example.com", Role: models.RoleAdmin, Enabled: true} require.NoError(t, db.Create(&target).Error) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -3021,7 +2932,6 @@ func TestUserHandler_UpdateUser_LastAdminDisable(t *testing.T) { disabled := false - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -3055,7 +2965,6 @@ func TestUserHandler_UpdateUser_WithSessionInvalidation(t *testing.T) { handler := NewUserHandler(db, authSvc) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") @@ -3091,7 +3000,6 @@ func TestUserHandler_UpdateUser_SessionInvalidationError(t *testing.T) { handler := NewUserHandler(mainDB, authSvc) - gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("role", "admin") diff --git a/backend/internal/api/handlers/user_integration_test.go b/backend/internal/api/handlers/user_integration_test.go index 7eed110f1..db2467596 100644 --- a/backend/internal/api/handlers/user_integration_test.go +++ b/backend/internal/api/handlers/user_integration_test.go @@ -31,7 +31,6 @@ func TestUserLoginAfterEmailChange(t *testing.T) { userHandler := NewUserHandler(db, nil) // Setup Router - gin.SetMode(gin.TestMode) r := gin.New() // Register Routes diff --git a/backend/internal/api/handlers/websocket_status_handler_test.go b/backend/internal/api/handlers/websocket_status_handler_test.go index 6f4cc8a23..f9274c5f6 100644 --- a/backend/internal/api/handlers/websocket_status_handler_test.go +++ b/backend/internal/api/handlers/websocket_status_handler_test.go @@ -15,7 +15,6 @@ import ( ) func TestWebSocketStatusHandler_GetConnections(t *testing.T) { - gin.SetMode(gin.TestMode) tracker := services.NewWebSocketTracker() handler := NewWebSocketStatusHandler(tracker) @@ -65,7 +64,6 @@ func TestWebSocketStatusHandler_GetConnections(t *testing.T) { } func TestWebSocketStatusHandler_GetConnectionsEmpty(t *testing.T) { - gin.SetMode(gin.TestMode) tracker := services.NewWebSocketTracker() handler := NewWebSocketStatusHandler(tracker) @@ -92,7 +90,6 @@ func TestWebSocketStatusHandler_GetConnectionsEmpty(t *testing.T) { } func TestWebSocketStatusHandler_GetStats(t *testing.T) { - gin.SetMode(gin.TestMode) tracker := services.NewWebSocketTracker() handler := NewWebSocketStatusHandler(tracker) @@ -141,7 +138,6 @@ func TestWebSocketStatusHandler_GetStats(t *testing.T) { } func TestWebSocketStatusHandler_GetStatsEmpty(t *testing.T) { - gin.SetMode(gin.TestMode) tracker := services.NewWebSocketTracker() handler := NewWebSocketStatusHandler(tracker) diff --git a/backend/internal/models/security_decision.go b/backend/internal/models/security_decision.go index 709c6c86c..7f9ff1397 100644 --- a/backend/internal/models/security_decision.go +++ b/backend/internal/models/security_decision.go @@ -9,11 +9,16 @@ import ( type SecurityDecision struct { ID uint `json:"-" gorm:"primaryKey"` UUID string `json:"uuid" gorm:"uniqueIndex"` - Source string `json:"source" gorm:"index"` // e.g., crowdsec, waf, ratelimit, manual - Action string `json:"action" gorm:"index"` // allow, block, challenge - IP string `json:"ip" gorm:"index"` + Source string `json:"source" gorm:"index;compositeIndex:idx_sd_source_created;compositeIndex:idx_sd_source_scenario_created;compositeIndex:idx_sd_source_ip_created"` // e.g., crowdsec, waf, ratelimit, manual + Action string `json:"action" gorm:"index"` // allow, block, challenge + IP string `json:"ip" gorm:"index;compositeIndex:idx_sd_source_ip_created"` Host string `json:"host" gorm:"index"` // optional RuleID string `json:"rule_id" gorm:"index"` Details string `json:"details" gorm:"type:text"` - CreatedAt time.Time `json:"created_at" gorm:"index"` + CreatedAt time.Time `json:"created_at" gorm:"index;compositeIndex:idx_sd_source_created,sort:desc;compositeIndex:idx_sd_source_scenario_created,sort:desc;compositeIndex:idx_sd_source_ip_created,sort:desc"` + + // Dashboard enrichment fields (Issue #26, PR-1) + Scenario string `json:"scenario,omitempty" gorm:"index;compositeIndex:idx_sd_source_scenario_created"` + Country string `json:"country,omitempty" gorm:"index;size:2"` + ExpiresAt *time.Time `json:"expires_at,omitempty" gorm:"index"` } diff --git a/configs/crowdsec/install_hub_items.sh b/configs/crowdsec/install_hub_items.sh index dc1b337fa..c2a2f214e 100644 --- a/configs/crowdsec/install_hub_items.sh +++ b/configs/crowdsec/install_hub_items.sh @@ -7,42 +7,45 @@ set -e echo "Installing CrowdSec hub items for Charon..." -# Update hub index first -echo "Updating hub index..." -cscli hub update 2>/dev/null || echo "Warning: Failed to update hub index" +# Hub index update is handled by the entrypoint before this script is called. +# Do not duplicate it here — a redundant update adds ~3s to startup for no benefit. # Install Caddy log parser (if available) # Note: crowdsecurity/caddy-logs may not exist yet - check hub if cscli parsers inspect crowdsecurity/caddy-logs >/dev/null 2>&1; then echo "Installing Caddy log parser..." - cscli parsers install crowdsecurity/caddy-logs --force 2>/dev/null || true + cscli parsers install crowdsecurity/caddy-logs --force || echo "⚠️ Failed to install crowdsecurity/caddy-logs" else echo "Caddy-specific parser not available, using HTTP parser..." fi # Install base HTTP parsers (always needed) echo "Installing base parsers..." -cscli parsers install crowdsecurity/http-logs --force 2>/dev/null || true -cscli parsers install crowdsecurity/syslog-logs --force 2>/dev/null || true -cscli parsers install crowdsecurity/geoip-enrich --force 2>/dev/null || true +cscli parsers install crowdsecurity/http-logs --force || echo "⚠️ Failed to install crowdsecurity/http-logs" +cscli parsers install crowdsecurity/syslog-logs --force || echo "⚠️ Failed to install crowdsecurity/syslog-logs" +cscli parsers install crowdsecurity/geoip-enrich --force || echo "⚠️ Failed to install crowdsecurity/geoip-enrich" # Install HTTP scenarios for attack detection echo "Installing HTTP scenarios..." -cscli scenarios install crowdsecurity/http-probing --force 2>/dev/null || true -cscli scenarios install crowdsecurity/http-sensitive-files --force 2>/dev/null || true -cscli scenarios install crowdsecurity/http-backdoors-attempts --force 2>/dev/null || true -cscli scenarios install crowdsecurity/http-path-traversal-probing --force 2>/dev/null || true -cscli scenarios install crowdsecurity/http-xss-probing --force 2>/dev/null || true -cscli scenarios install crowdsecurity/http-sqli-probing --force 2>/dev/null || true -cscli scenarios install crowdsecurity/http-generic-bf --force 2>/dev/null || true +cscli scenarios install crowdsecurity/http-probing --force || echo "⚠️ Failed to install crowdsecurity/http-probing" +cscli scenarios install crowdsecurity/http-sensitive-files --force || echo "⚠️ Failed to install crowdsecurity/http-sensitive-files" +cscli scenarios install crowdsecurity/http-backdoors-attempts --force || echo "⚠️ Failed to install crowdsecurity/http-backdoors-attempts" +cscli scenarios install crowdsecurity/http-path-traversal-probing --force || echo "⚠️ Failed to install crowdsecurity/http-path-traversal-probing" +cscli scenarios install crowdsecurity/http-xss-probing --force || echo "⚠️ Failed to install crowdsecurity/http-xss-probing" +cscli scenarios install crowdsecurity/http-sqli-probing --force || echo "⚠️ Failed to install crowdsecurity/http-sqli-probing" +cscli scenarios install crowdsecurity/http-generic-bf --force || echo "⚠️ Failed to install crowdsecurity/http-generic-bf" # Install CVE collection for known vulnerabilities echo "Installing CVE collection..." -cscli collections install crowdsecurity/http-cve --force 2>/dev/null || true +cscli collections install crowdsecurity/http-cve --force || echo "⚠️ Failed to install crowdsecurity/http-cve" # Install base HTTP collection (bundles common scenarios) echo "Installing base HTTP collection..." -cscli collections install crowdsecurity/base-http-scenarios --force 2>/dev/null || true +cscli collections install crowdsecurity/base-http-scenarios --force || echo "⚠️ Failed to install crowdsecurity/base-http-scenarios" + +# Install Caddy collection (parser + scenarios for Caddy access logs) +echo "Installing Caddy collection..." +cscli collections install crowdsecurity/caddy --force || echo "⚠️ Failed to install crowdsecurity/caddy" # Verify installation echo "" diff --git a/docs/features.md b/docs/features.md index 139348d81..6d002d8a6 100644 --- a/docs/features.md +++ b/docs/features.md @@ -78,6 +78,24 @@ Protect your applications using behavior-based threat detection powered by a glo --- +### 📊 CrowdSec Dashboard + +See your security posture at a glance. The CrowdSec Dashboard shows attack trends, active bans, top offenders, and scenario breakdowns—all from within Charon's Security section. + +**Highlights:** + +- **Summary Cards** — Total bans, active bans, unique IPs, and top scenario at a glance +- **Interactive Charts** — Ban timeline, top attacking IPs, and attack type breakdown +- **Alerts Feed** — Live view of CrowdSec alerts with pagination +- **Time Range Selector** — Filter data by 1 hour, 6 hours, 24 hours, 7 days, or 30 days +- **Export** — Download decisions as CSV or JSON for external analysis + +No SSH required. No CLI commands. Just open the Dashboard tab and see what's happening. + +→ [Learn More](features/crowdsec.md) + +--- + ### 🔐 Access Control Lists (ACLs) Define exactly who can access what. Block specific countries, allow only certain IP ranges, or require authentication for sensitive applications. Fine-grained rules give you complete control. diff --git a/docs/issues/crowdsec-dashboard-manual-test.md b/docs/issues/crowdsec-dashboard-manual-test.md new file mode 100644 index 000000000..8e35aa382 --- /dev/null +++ b/docs/issues/crowdsec-dashboard-manual-test.md @@ -0,0 +1,162 @@ +--- +title: "Manual Test Plan - Issue #26: CrowdSec Dashboard Integration" +status: Open +priority: High +assignee: QA +labels: testing, backend, frontend, security +--- + +# Test Objective + +Confirm that the CrowdSec Dashboard tab displays accurate security metrics, charts render correctly, time range filtering works, alerts paginate properly, and export produces valid output. + +# Scope + +- In scope: Dashboard tab navigation, summary cards, chart rendering, time range selector, active decisions table, alerts feed, CSV/JSON export, keyboard navigation, screen reader compatibility. +- Out of scope: CrowdSec engine start/stop/configuration, bouncer registration, existing security toggle behavior. + +# Prerequisites + +- Charon instance running with CrowdSec enabled and at least a few recorded decisions. +- Admin account credentials. +- Browser DevTools available for network inspection. +- Screen reader available for accessibility testing (e.g., NVDA, VoiceOver). + +# Manual Scenarios + +## 1) Dashboard Tab Navigation + +- [ ] Navigate to `/security/crowdsec`. +- [ ] **Expected**: Two tabs are visible — "Configuration" and "Dashboard." +- [ ] Click the "Dashboard" tab. +- [ ] **Expected**: The dashboard loads with summary cards, charts, and the decisions table. +- [ ] Click the "Configuration" tab. +- [ ] **Expected**: The existing CrowdSec configuration interface appears unchanged. +- [ ] Click back to "Dashboard." +- [ ] **Expected**: Dashboard state is preserved (same time range, same data). + +## 2) Summary Cards Accuracy + +- [ ] Open the Dashboard tab with the default 24h time range. +- [ ] **Expected**: Four summary cards are displayed — Total Bans, Active Bans, Unique IPs, Top Scenario. +- [ ] Compare the "Active Bans" count against `cscli decisions list` output from the container. +- [ ] **Expected**: The counts match (within the 30-second cache window). +- [ ] Check that the trend indicator (percentage change) is visible on the Total Bans card. + +## 3) Chart Rendering + +- [ ] Confirm the ban timeline chart (area chart) renders with data points. +- [ ] **Expected**: The X-axis shows time labels and the Y-axis shows ban counts. +- [ ] Confirm the top attacking IPs chart (horizontal bar chart) renders. +- [ ] **Expected**: Up to 10 IP addresses are listed with proportional bars. +- [ ] Confirm the scenario breakdown chart (pie/donut chart) renders. +- [ ] **Expected**: Slices represent different CrowdSec scenarios with a legend. +- [ ] Hover over data points in each chart. +- [ ] **Expected**: Tooltips appear with relevant values. + +## 4) Time Range Switching + +- [ ] Select the "1h" time range. +- [ ] **Expected**: All cards and charts update to reflect the last 1 hour of data. +- [ ] Select "7d." +- [ ] **Expected**: Data expands to show the last 7 days. +- [ ] Select "30d." +- [ ] **Expected**: Data expands to show the last 30 days. Charts may show wider time buckets. +- [ ] Rapidly toggle between "1h" and "30d" several times. +- [ ] **Expected**: No stale data, no visual glitches, and no console errors. The most recently selected range is always displayed. + +## 5) Active Decisions Table + +- [ ] Scroll to the active decisions table on the Dashboard. +- [ ] **Expected**: Table columns include IP, Scenario, Duration, Type (ban/captcha), Origin, and Time Remaining. +- [ ] Verify that the "Time Remaining" column shows a countdown or human-readable time. +- [ ] If there are more than 10 active decisions, confirm pagination or scrolling works. +- [ ] If there are zero active decisions, confirm a placeholder message is shown (e.g., "No active decisions"). + +## 6) Alerts Feed + +- [ ] Scroll to the alerts section of the Dashboard. +- [ ] **Expected**: A list of recent CrowdSec alerts is displayed with timestamps and scenario names. +- [ ] If there are enough alerts, confirm that pagination controls are present and functional. +- [ ] Click "Next" on the pagination. +- [ ] **Expected**: The next page of alerts loads without duplicates. +- [ ] Click "Previous." +- [ ] **Expected**: Returns to the first page with the original data. + +## 7) CSV Export + +- [ ] Click the "Export" button on the Dashboard. +- [ ] Select "CSV" as the format. +- [ ] **Expected**: A `.csv` file downloads to your machine. +- [ ] Open the file in a text editor or spreadsheet application. +- [ ] **Expected**: Columns match the decisions table (IP, Scenario, Duration, Type, etc.) and rows contain valid data. + +## 8) JSON Export + +- [ ] Click the "Export" button on the Dashboard. +- [ ] Select "JSON" as the format. +- [ ] **Expected**: A `.json` file downloads to your machine. +- [ ] Open the file in a text editor. +- [ ] **Expected**: Valid JSON array of decision objects. Fields match the decisions table. + +## 9) Keyboard Navigation + +- [ ] Use `Tab` to navigate from the tab bar to the summary cards, then to the charts, then to the table. +- [ ] **Expected**: Focus moves in a logical order. A visible focus indicator is shown on each interactive element. +- [ ] Use `Enter` or `Space` to activate the time range selector buttons. +- [ ] **Expected**: The selected time range changes and data updates. +- [ ] Use `Tab` to reach the "Export" button, then press `Enter`. +- [ ] **Expected**: The export dialog or menu opens. + +## 10) Screen Reader Compatibility + +- [ ] Enable a screen reader (NVDA, VoiceOver, or similar). +- [ ] Navigate to the Dashboard tab. +- [ ] **Expected**: The tab bar is announced correctly with "Configuration" and "Dashboard" tab names. +- [ ] Navigate to the summary cards. +- [ ] **Expected**: Each card's label and value is announced (e.g., "Total Bans: 1247"). +- [ ] Navigate to the charts. +- [ ] **Expected**: Charts have accessible labels or descriptions (e.g., "Ban Timeline Chart"). +- [ ] Navigate to the decisions table. +- [ ] **Expected**: Table headers and cell values are announced correctly. + +# Edge Cases + +## 11) Empty CrowdSec Data + +- [ ] Disable CrowdSec or test on an instance with zero recorded decisions. +- [ ] Open the Dashboard tab. +- [ ] **Expected**: Summary cards show `0` values. Charts show an empty state or placeholder. The decisions table shows "No active decisions." No errors in the console. + +## 12) Large Number of Decisions + +- [ ] Test on an instance with 1,000+ recorded decisions (or simulate with test data). +- [ ] Open the Dashboard tab with the "30d" time range. +- [ ] **Expected**: Dashboard loads within 2 seconds. Charts render without performance issues. Pagination handles the large dataset. + +## 13) CrowdSec LAPI Unavailable + +- [ ] Stop the CrowdSec container while Charon is running. +- [ ] Open the Dashboard tab. +- [ ] **Expected**: Historical data from the database still renders. Active decisions and alerts show an error or "unavailable" state. No unhandled errors in the UI. + +## 14) Rapid Time Range Switching Under Load + +- [ ] On an instance with significant data, rapidly click through all five time ranges in quick succession. +- [ ] **Expected**: Only the final selection's data is displayed. No race conditions, stale data, or flickering. + +# Expected Results + +- Dashboard tab loads and displays all components (cards, charts, table, alerts). +- Summary card numbers match CrowdSec LAPI and database records within the cache window. +- Charts render with correct data for the selected time range. +- Export produces valid CSV and JSON files with matching data. +- Keyboard and screen reader users can navigate and interact with all dashboard elements. +- Edge cases (empty data, LAPI down, large datasets) are handled gracefully. + +# Regression Checks + +- [ ] Confirm the existing CrowdSec Configuration tab is unchanged in behavior and layout. +- [ ] Confirm CrowdSec start/stop/restart functionality is unaffected. +- [ ] Confirm existing security toggles (ACL, WAF, Rate Limiting) are unaffected. +- [ ] Confirm no new console errors appear on pages outside the Dashboard. diff --git a/docs/plans/current_spec.md b/docs/plans/current_spec.md index 91fe5bed2..8c9356d4e 100644 --- a/docs/plans/current_spec.md +++ b/docs/plans/current_spec.md @@ -1,592 +1,159 @@ -# Ntfy Notification Provider — Implementation Specification +# CrowdSec Hub Bootstrapping on Container Startup -## 1. Introduction +## Problem Statement -### Overview +After a container rebuild, CrowdSec has **zero collections installed** and a **stale hub index**. When users (or the backend) attempt to install collections, they encounter hash mismatch errors because the hub index bundled in the image at build time is outdated by the time the container runs. -Add **Ntfy** () as a notification provider in Charon, following -the same wrapper pattern used by Gotify, Telegram, Slack, and Pushover. Ntfy is -an HTTP-based pub/sub notification service that supports self-hosted and -cloud-hosted instances. Users publish messages by POSTing JSON to a topic URL, -optionally with an auth token. +The root cause is twofold: -### Objectives +1. `cscli hub update` in the entrypoint only runs if `.index.json` is missing — not if it is stale. +2. `install_hub_items.sh` (which does call `cscli hub update`) is gated behind `SECURITY_CROWDSEC_MODE=local`, an env var that is **deprecated** and no longer set by default. The entrypoint checks `$SECURITY_CROWDSEC_MODE`, but the backend reads `CERBERUS_SECURITY_CROWDSEC_MODE` / `CHARON_SECURITY_CROWDSEC_MODE` — a naming mismatch that means the entrypoint gate never opens. -1. Users can create/edit/delete an Ntfy notification provider via the Management UI. -2. Ntfy dispatches support all three template modes (minimal, detailed, custom). -3. Ntfy respects the global notification engine kill-switch and its own per-provider feature flag. -4. Security: auth tokens are stored securely (never exposed in API responses or logs). -5. Full E2E and unit test coverage matching the existing provider test suite. +## Current State Analysis ---- +### Dockerfile (build-time) -## 2. Research Findings +| Aspect | What Happens | +|---|---| +| CrowdSec binaries | Built from source in `crowdsec-builder` stage, copied to `/usr/local/bin/{crowdsec,cscli}` | +| Config template | Source config copied to `/etc/crowdsec.dist/` | +| Hub index | **Not pre-populated** — no `cscli hub update` at build time | +| Collections | **Not installed** at build time | +| Symlink | `/etc/crowdsec` → `/app/data/crowdsec/config` created as root before `USER charon` | +| Helper scripts | `install_hub_items.sh` and `register_bouncer.sh` copied to `/usr/local/bin/` | -### Existing Architecture +### Entrypoint (`.docker/docker-entrypoint.sh`, runtime) -Charon's notification engine does **not** use a Go interface pattern. Instead, it -routes on string type values (`"discord"`, `"gotify"`, `"webhook"`, etc.) across -~15 switch/case + hardcoded lists in both backend and frontend. +| Step | What Happens | Problem | +|---|---|---| +| Config init | Copies `/etc/crowdsec.dist/*` → `/app/data/crowdsec/config/` on first run | Works correctly | +| Symlink verify | Confirms `/etc/crowdsec` → `/app/data/crowdsec/config` | Works correctly | +| LAPI port fix | `sed` replaces `:8080` → `:8085` in config files | Works correctly | +| Hub update (L313-315) | `cscli hub update` runs **only if** `/etc/crowdsec/hub/.index.json` does not exist | **Bug**: stale index is never refreshed | +| Machine registration | `cscli machines add -a --force` | Works correctly | +| Hub items install (L325-328) | Calls `install_hub_items.sh` **only if** `$SECURITY_CROWDSEC_MODE = "local"` | **Bug**: env var is deprecated, never set; wrong var name vs backend | +| Ownership fix | `chown -R charon:charon` on CrowdSec dirs | Works correctly | -**Key code paths per provider type:** +### `install_hub_items.sh` (when invoked) -| Layer | Location | Mechanism | -|-------|----------|-----------| -| Model | `backend/internal/models/notification_provider.go` | Generic — no per-type changes needed | -| Service — type allowlist | `notification_service.go:139` `isSupportedNotificationProviderType()` | `switch` on type string | -| Service — flag routing | `notification_service.go:148` `isDispatchEnabled()` | `switch` → feature flag lookup | -| Service — dispatch | `notification_service.go:381` `sendJSONPayload()` | Type-specific validation + URL / header construction | -| Feature flags | `notifications/feature_flags.go` | Const strings for settings DB keys | -| Router | `notifications/router.go:10` `ShouldUseNotify()` | `switch` on type → flag map lookup | -| Handler — create validation | `notification_provider_handler.go:185` | Hardcoded `!=` chain | -| Handler — update validation | `notification_provider_handler.go:245` | Hardcoded `!=` chain | -| Handler — URL validation | `notification_provider_handler.go:372` | Slack special-case (optional URL) | -| Frontend — type array | `api/notifications.ts:3` | `SUPPORTED_NOTIFICATION_PROVIDER_TYPES` const | -| Frontend — sanitize | `api/notifications.ts` `sanitizeProviderForWriteAction()` | Token mapping per type | -| Frontend — form | `pages/Notifications.tsx` | `