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/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/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/docker-build.yml b/.github/workflows/docker-build.yml index 9029ac243..358f3d28d 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -565,7 +565,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 +594,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 @@ -724,14 +724,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 +739,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 +747,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..a93a469f2 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 }} diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 44f8e8965..d0556a135 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -333,7 +333,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 @@ -451,7 +451,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/quality-checks.yml b/.github/workflows/quality-checks.yml index d14dec74a..af09cf329 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 }} 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/security-pr.yml b/.github/workflows/security-pr.yml index 7e05d9de5..895b90a6c 100644 --- a/.github/workflows/security-pr.yml +++ b/.github/workflows/security-pr.yml @@ -385,7 +385,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@a899987af240c0578ed84ce13c02319a693e168f 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) }} diff --git a/.github/workflows/security-weekly-rebuild.yml b/.github/workflows/security-weekly-rebuild.yml index 1efc9f40c..96b59dc77 100644 --- a/.github/workflows/security-weekly-rebuild.yml +++ b/.github/workflows/security-weekly-rebuild.yml @@ -113,7 +113,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..cdb3ea9ba 100644 --- a/.github/workflows/supply-chain-pr.yml +++ b/.github/workflows/supply-chain-pr.yml @@ -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/.grype.yaml b/.grype.yaml index 945b8297b..dfe28943c 100644 --- a/.grype.yaml +++ b/.grype.yaml @@ -284,6 +284,133 @@ 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 + # Match exclusions (patterns to ignore during scanning) # Use sparingly - prefer specific CVE suppressions above match: diff --git a/.trivyignore b/.trivyignore index 199b38ecb..7b1669255 100644 --- a/.trivyignore +++ b/.trivyignore @@ -78,3 +78,37 @@ GHSA-jqcq-xjh3-6g23 # See also: .grype.yaml for full justification # exp: 2026-04-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 a435aa5f1..a6b59264a 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.58 # renovate: datasource=go depName=github.com/corazawaf/coraza-caddy -ARG CORAZA_CADDY_VERSION=2.2.0 +ARG CORAZA_CADDY_VERSION=2.3.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 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/backend/go.mod b/backend/go.mod index 44a2e22b3..d108d1853 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.38 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 diff --git a/backend/go.sum b/backend/go.sum index 5c30b3062..82696b11c 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -41,6 +41,8 @@ github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9 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/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.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= @@ -103,6 +105,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE 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.38 h1:tDUzL85kMvOrvpCt8P64SbGgVFtJB11GPi2AdmITgb4= +github.com/mattn/go-sqlite3 v1.14.38/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 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= 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..35243efd9 --- /dev/null +++ b/backend/internal/api/handlers/crowdsec_dashboard.go @@ -0,0 +1,627 @@ +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), + d.ExpiresAt.UTC().Format(time.RFC3339), + }) + } + 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..d439b8b54 --- /dev/null +++ b/backend/internal/api/handlers/crowdsec_dashboard_cache.go @@ -0,0 +1,62 @@ +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.Lock() + defer c.mu.Unlock() + + entry, ok := c.entries[key] + if !ok { + return nil, false + } + if time.Now().After(entry.expiresAt) { + delete(c.entries, key) + return nil, false + } + return entry.data, true +} + +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..157012a2f 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,20 @@ 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) + _ = h.Security.LogDecision(&models.SecurityDecision{ + IP: ip, + Action: "block", + Source: "crowdsec", + RuleID: reason, + Scenario: "manual", + ExpiresAt: time.Now().Add(parsedDur), + }) + } + h.dashCache.Invalidate("dashboard") } // UnbanIP removes a ban for an IP address @@ -2313,6 +2335,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 +2734,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_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..f3be817dd 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{})) 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..d5befa2f2 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" gorm:"index;compositeIndex:idx_sd_source_scenario_created"` + Country string `json:"country" gorm:"index;size:2"` + ExpiresAt time.Time `json:"expires_at" gorm:"index"` } 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..0a1ff69bf 100644 --- a/docs/plans/current_spec.md +++ b/docs/plans/current_spec.md @@ -1,592 +1,1124 @@ -# Ntfy Notification Provider — Implementation Specification +# CrowdSec Dashboard Integration — Implementation Specification -## 1. Introduction +**Issue:** #26 +**Version:** 1.1 +**Status:** Draft — Post-Supervisor Review -### Overview +--- + +## 1. Executive Summary + +### What We're Building + +A metrics and analytics dashboard for CrowdSec within Charon's existing Security section. This adds visualization, aggregation, and export capabilities to the CrowdSec module — surfacing data that today is only available via CLI or raw LAPI queries. -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. +### Why -### Objectives +CrowdSec is already operationally integrated (start/stop, config, bouncer registration, decisions, console enrollment). What's missing is **visibility**: users cannot see attack trends, scenario breakdowns, ban history, or top offenders without SSH-ing into the container and running `cscli` commands. A dashboard closes this gap and makes Charon's security posture observable from the UI. -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. +### Success Metrics + +| Metric | Target | +|--------|--------| +| Issue #26 checklist tasks complete | 8/8 | +| New backend aggregation endpoints covered by unit tests | ≥ 85% line coverage | +| New frontend components covered by Vitest unit tests | ≥ 85% line coverage | +| E2E tests for dashboard page passing | All browsers (Firefox, Chromium, WebKit) | +| Dashboard page initial load time | < 2 seconds on cached data | +| No new CRITICAL/HIGH security findings | GORM scanner, CodeQL, Trivy | --- -## 2. Research Findings +## 2. Requirements (EARS Notation) -### Existing Architecture +### R1: Metrics Dashboard Tab -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. +**WHEN** the user navigates to `/security/crowdsec`, **THE SYSTEM SHALL** display a "Dashboard" tab alongside the existing configuration interface, showing summary statistics (total bans, active bans, unique IPs, top scenario). -**Key code paths per provider type:** +### R2: Active Bans with Reasons -| 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` | `