diff --git a/.github/prompts/codecov-patch-coverage-fix.prompt.md b/.github/prompts/codecov-patch-coverage-fix.prompt.md new file mode 100644 index 00000000..fab8614c --- /dev/null +++ b/.github/prompts/codecov-patch-coverage-fix.prompt.md @@ -0,0 +1,208 @@ +--- +agent: 'agent' +description: 'Generate targeted tests to achieve 100% Codecov patch coverage when CI reports uncovered lines' +tools: ['changes', 'search/codebase', 'edit/editFiles', 'fetch', 'findTestFiles', 'problems', 'runCommands', 'runTasks', 'runTests', 'search', 'search/searchResults', 'runCommands/terminalLastCommand', 'runCommands/terminalSelection', 'testFailure', 'usages'] +--- + +# Codecov Patch Coverage Fix + +You are a senior test engineer with deep expertise in test-driven development, code coverage analysis, and writing effective unit and integration tests. You have extensive experience with: + +- Interpreting Codecov reports and understanding patch vs project coverage +- Writing targeted tests that exercise specific code paths and edge cases +- Go testing patterns (`testing` package, table-driven tests, mocks, test helpers) +- JavaScript/TypeScript testing with Vitest, Jest, and React Testing Library +- Achieving 100% patch coverage without writing redundant or brittle tests + +## Primary Objective + +Analyze the provided Codecov comment or report and generate the minimum set of high-quality tests required to achieve **100% patch coverage** on all modified lines. Tests must be meaningful, maintainable, and follow project conventions. + +## Input Requirements + +The user will provide ONE of the following: + +1. **Codecov Comment (Copy/Pasted)**: The full text of a Codecov bot comment from a PR +2. **Codecov Report Link**: A URL to the Codecov coverage report for the PR +3. **Specific File + Lines**: Direct reference to files and uncovered line ranges + +### Example Input Formats + +**Format 1 - Codecov Comment:** +``` +Codecov Report +Attention: Patch coverage is 75.00000% with 4 lines in your changes missing coverage. +Project coverage is 82.45%. Comparing base (abc123) to head (def456). + +Files with missing coverage: +| File | Coverage | Lines | +|------|----------|-------| +| backend/internal/services/mail_service.go | 75.00% | 45-48 | +``` + +**Format 2 - Link:** +`https://app.codecov.io/gh/Owner/Repo/pull/123` + +**Format 3 - Direct Reference:** +`backend/internal/services/mail_service.go lines 45-48, 62, 78-82` + +## Execution Protocol + +### Phase 1: Parse and Identify + +1. **Extract Coverage Data**: Parse the Codecov comment/report to identify: + - Files with missing patch coverage + - Specific line numbers or ranges that are uncovered + - The current patch coverage percentage + - The target coverage (always 100% for patch coverage) + +2. **Document Findings**: Create a structured list: + ``` + UNCOVERED FILES: + - FILE-001: [path/to/file.go] - Lines: [45-48, 62] + - FILE-002: [path/to/other.ts] - Lines: [23, 67-70] + ``` + +### Phase 2: Analyze Uncovered Code + +For each file with missing coverage: + +1. **Read the Source File**: Use the codebase tool to read the file and understand: + - What the uncovered lines do + - What functions/methods contain the uncovered code + - What conditions or branches lead to those lines + - Any dependencies or external calls + +2. **Identify Code Paths**: Determine what inputs, states, or conditions would cause execution of the uncovered lines: + - Error handling paths + - Edge cases (nil, empty, boundary values) + - Conditional branches (if/else, switch cases) + - Loop iterations (zero, one, many) + +3. **Find Existing Tests**: Locate the corresponding test file(s): + - Go: `*_test.go` in the same package + - TypeScript/JavaScript: `*.test.ts`, `*.spec.ts`, or in `__tests__/` directory + +### Phase 3: Generate Tests + +For each uncovered code path: + +1. **Follow Project Patterns**: Analyze existing tests to match: + - Test naming conventions + - Setup/teardown patterns + - Mocking strategies + - Assertion styles + - Table-driven test structures (especially for Go) + +2. **Write Targeted Tests**: Create tests that specifically exercise the uncovered lines: + - One test case per distinct code path + - Use descriptive test names that explain the scenario + - Include appropriate setup and teardown + - Use meaningful assertions that verify behavior, not just coverage + +3. **Test Quality Standards**: + - Tests must be deterministic (no flaky tests) + - Tests must be independent (no shared state between tests) + - Tests must be fast (mock external dependencies) + - Tests must be readable (clear arrange-act-assert structure) + +### Phase 4: Validate + +1. **Run the Tests**: Execute the new tests to ensure they pass +2. **Verify Coverage**: If possible, run coverage locally to confirm the lines are now covered +3. **Check for Regressions**: Ensure existing tests still pass + +## Language-Specific Guidelines + +### Go Testing + +```go +// Table-driven test pattern for multiple cases +func TestFunctionName_Scenario(t *testing.T) { + tests := []struct { + name string + input InputType + want OutputType + wantErr bool + }{ + { + name: "descriptive case name", + input: InputType{...}, + want: OutputType{...}, + }, + // Additional cases for uncovered paths + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := FunctionName(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("FunctionName() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("FunctionName() = %v, want %v", got, tt.want) + } + }) + } +} +``` + +### TypeScript/JavaScript Testing (Vitest) + +```typescript +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +describe('ComponentOrFunction', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should handle specific edge case for uncovered line', () => { + // Arrange + const input = createTestInput({ edgeCase: true }); + + // Act + const result = functionUnderTest(input); + + // Assert + expect(result).toMatchObject({ expected: 'value' }); + }); + + it('should handle error condition at line XX', async () => { + // Arrange - setup condition that triggers error path + vi.spyOn(dependency, 'method').mockRejectedValue(new Error('test error')); + + // Act & Assert + await expect(functionUnderTest()).rejects.toThrow('expected error message'); + }); +}); +``` + +## Output Requirements + +1. **Coverage Triage Report**: Document each uncovered file/line and the test strategy +2. **Test Code**: Complete, runnable test code placed in appropriate test files +3. **Execution Results**: Output from running the tests showing they pass +4. **Coverage Verification**: Confirmation that the previously uncovered lines are now exercised + +## Constraints + +- **Do NOT relax coverage thresholds** - always aim for 100% patch coverage +- **Do NOT write tests that only exist for coverage** - tests must verify behavior +- **Do NOT modify production code** unless a bug is discovered during testing +- **Do NOT skip error handling paths** - these often cause coverage gaps +- **Do NOT create flaky tests** - all tests must be deterministic + +## Success Criteria + +- [ ] All files from Codecov report have been addressed +- [ ] All previously uncovered lines now have test coverage +- [ ] All new tests pass consistently +- [ ] All existing tests continue to pass +- [ ] Test code follows project conventions and patterns +- [ ] Tests are meaningful and maintainable, not just coverage padding + +## Begin + +Please provide the Codecov comment, report link, or file/line references that you want me to analyze and fix. diff --git a/backend/internal/api/handlers/crowdsec_handler_test.go b/backend/internal/api/handlers/crowdsec_handler_test.go index 89a0acb6..79aee2e5 100644 --- a/backend/internal/api/handlers/crowdsec_handler_test.go +++ b/backend/internal/api/handlers/crowdsec_handler_test.go @@ -2222,3 +2222,109 @@ func TestCrowdsecHandler_GetCachedPreset_EmptySlug(t *testing.T) { // Empty slug should result in 404 (route not matched) or 400 require.True(t, w.Code == http.StatusNotFound || w.Code == http.StatusBadRequest) } + +// 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{} + h := NewCrowdsecHandler(db, fe, "/bin/false", tmpDir) + + r := gin.New() + g := r.Group("/api/v1") + h.RegisterRoutes(g) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/crowdsec/start", http.NoBody) + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + require.NoError(t, err) + require.Equal(t, "started", response["status"]) +} + +// 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} + h := NewCrowdsecHandler(db, fe, "/bin/false", tmpDir) + + // Create initial SecurityConfig + cfg := models.SecurityConfig{ + UUID: "default", + Name: "Default", + Enabled: true, + CrowdSecMode: "local", + } + require.NoError(t, db.Create(&cfg).Error) + + r := gin.New() + g := r.Group("/api/v1") + h.RegisterRoutes(g) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/crowdsec/stop", http.NoBody) + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + + // Verify SecurityConfig was updated + var updatedCfg models.SecurityConfig + require.NoError(t, db.First(&updatedCfg).Error) + require.Equal(t, "disabled", updatedCfg.CrowdSecMode) + require.False(t, updatedCfg.Enabled) +} + +// 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()) + c1.Set("userID", 123) + actor1 := actorFromContext(c1) + require.Equal(t, "user:123", actor1) + + // Test without userID + c2, _ := gin.CreateTestContext(httptest.NewRecorder()) + actor2 := actorFromContext(c2) + require.Equal(t, "unknown", actor2) +} + +// 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 + envKey string + envValue string + expected bool + }{ + {"FEATURE_CERBERUS_ENABLED=true", "FEATURE_CERBERUS_ENABLED", "true", true}, + {"FEATURE_CERBERUS_ENABLED=false", "FEATURE_CERBERUS_ENABLED", "false", false}, + {"CERBERUS_ENABLED=1", "CERBERUS_ENABLED", "1", true}, + {"CERBERUS_ENABLED=0", "CERBERUS_ENABLED", "0", false}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Setenv(tc.envKey, tc.envValue) + db := setupCrowdDB(t) + tmpDir := t.TempDir() + h := NewCrowdsecHandler(db, &fakeExec{}, "/bin/false", tmpDir) + + result := h.isCerberusEnabled() + require.Equal(t, tc.expected, result) + }) + } +}