* Go rewrite * chore(cr): apply suggestions Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Ismo Vuorinen <ismo@ivuorinen.net> * 📝 CodeRabbit Chat: Add NoOpClient to fail2ban and initialize when skip flag is true * 📝 CodeRabbit Chat: Fix malformed if-else structure and add no-op client for skip-only commands * 📝 CodeRabbit Chat: Fix malformed if-else structure and add no-op client for skip-only commands * fix(main): correct no-op branch syntax (#10) * chore(gitignore): ignore env and binary files (#11) * chore(config): remove indent_size for go files (#12) * feat(cli): inject version via ldflags (#13) * fix(security): validate filter parameter to prevent path traversal (#15) * chore(repo): anchor ignore for build artifacts (#16) * chore(ci): use golangci-lint action (#17) * feat(fail2ban): expose GetLogDir (#19) * test(cmd): improve IP mock validation (#20) * chore(ci): update golanglint * fix(ci): golanglint * fix(ci): correct args indentation in pr-lint workflow (#21) * fix(ci): avoid duplicate releases (#22) * refactor(fail2ban): remove test check from OSRunner (#23) * refactor(fail2ban): make log and filter dirs configurable (#24) * fix(ci): create single release per tag (#14) Signed-off-by: Ismo Vuorinen <ismo@ivuorinen.net> * chore(dev): add codex setup script (#27) * chore(lint): enable staticcheck (#26) * chore(ci): verify golangci config (#28) * refactor(cmd): centralize env config (#29) * chore(dev): add pre-commit config (#30) * fix(ci): disable cgo in cross compile (#31) * fix(ci): fail on formatting issues (#32) * feat(cmd): add context to logs watch (#33) * chore: fixes, roadmap, claude.md, linting * chore: fixes, linting * fix(ci): gh actions update, fixes and tweaks * chore: use reviewdog actionlint * chore: use wow-rp-addons/actions-editorconfig-check * chore: combine agent instructions, add comments, fixes * chore: linting, fixes, go revive * chore(deps): update pre-commit hooks * chore: bump go to 1.21, pin workflows * fix: install tools in lint.yml * fix: sudo timeout * fix: service command injection * fix: memory exhaustion with large logs * fix: enhanced path traversal and file security vulns * fix: race conditions * fix: context support * chore: simplify fail2ban/ code * feat: major refactoring with GoReleaser integration and code consolidation - Add GoReleaser configuration for automated multi-platform releases - Support for Linux, macOS, Windows, and BSD builds - Docker images, Homebrew tap, and Linux packages (.deb, .rpm, .apk) - GitHub Actions workflow for release automation - Consolidate duplicate code and improve architecture - Extract common command helpers to cmd/helpers.go (~230 lines) - Remove duplicate MockClient implementation from tests (~250 lines) - Create context wrapper helpers in fail2ban/context_helpers.go - Standardize error messages in fail2ban/errors.go - Enhance validation and security - Add proper IP address validation with fail2ban.ValidateIP - Fix path traversal and command injection vulnerabilities - Improve thread-safety in MockClient with consistent ordering - Optimize documentation - Reduce CLAUDE.md from 190 to 81 lines (57% reduction) - Reduce TODO.md from 633 to 93 lines (85% reduction) - Move README.md to root directory with installation instructions - Improve test reliability - Fix race conditions and test flakiness - Add sorting to ensure deterministic test output - Enhance MockClient with configurable behavior * feat: comprehensive code quality improvements and documentation reorganization This commit represents a major overhaul of code quality, documentation structure, and development tooling: **Documentation & Structure:** - Move CODE_OF_CONDUCT.md from .github to root directory - Reorganize documentation with dedicated docs/ directory - Create comprehensive architecture, security, and testing documentation - Update all references and cross-links for new documentation structure **Code Quality & Linting:** - Add 120-character line length limit across all files via EditorConfig - Enable comprehensive linting with golines, lll, usetesting, gosec, and revive - Fix all 86 revive linter issues (unused parameters, missing export comments) - Resolve security issues (file permissions 0644 → 0600, gosec warnings) - Replace deprecated os.Setenv with t.Setenv in all tests - Configure golangci-lint with auto-fix capabilities and formatter integration **Development Tooling:** - Enhance pre-commit configuration with additional hooks and formatters - Update GoReleaser configuration with improved YAML formatting - Improve GitHub workflows and issue templates for CLI-specific context - Add comprehensive Makefile with proper dependency checking **Testing & Security:** - Standardize mock patterns and context wrapper implementations - Enhance error handling with centralized error constants - Improve concurrent access testing for thread safety * perf: implement major performance optimizations with comprehensive test coverage This commit introduces three significant performance improvements along with complete linting compliance and robust test coverage: **Performance Optimizations:** 1. **Time Parsing Cache (8.6x improvement)** - Add TimeParsingCache with sync.Map for caching parsed times - Implement object pooling for string builders to reduce allocations - Create optimized BanRecordParser with pooled string slices 2. **Gzip Detection Consolidation (55x improvement)** - Consolidate ~100 lines of duplicate gzip detection logic - Fast-path extension checking before magic byte detection - Unified GzipDetector with comprehensive file handling utilities 3. **Parallel Processing (2.5-5.0x improvement)** - Generic WorkerPool implementation for concurrent operations - Smart fallback to sequential processing for single operations - Context-aware cancellation support for long-running tasks - Applied to ban/unban operations across multiple jails **New Files Added:** - fail2ban/time_parser.go: Cached time parsing with global instances - fail2ban/ban_record_parser.go: Optimized ban record parsing - fail2ban/gzip_detection.go: Unified gzip handling utilities - fail2ban/parallel_processing.go: Generic parallel processing framework - cmd/parallel_operations.go: Command-level parallel operation support **Code Quality & Linting:** - Resolve all golangci-lint issues (0 remaining) - Add proper #nosec annotations for legitimate file operations - Implement sentinel errors replacing nil/nil anti-pattern - Fix context parameter handling and error checking **Comprehensive Test Coverage:** - 500+ lines of new tests with benchmarks validating all improvements - Concurrent access testing for thread safety - Edge case handling and error condition testing - Performance benchmarks demonstrating measured improvements **Modified Files:** - fail2ban/fail2ban.go: Integration with new optimized parsers - fail2ban/logs.go: Use consolidated gzip detection (-91 lines) - cmd/ban.go & cmd/unban.go: Add conditional parallel processing * test: comprehensive test infrastructure overhaul with real test data Major improvements to test code quality and organization: • Added comprehensive test data infrastructure with 6 anonymized log files • Extracted common test helpers reducing ~200 lines to ~50 reusable functions • Enhanced ban record parser tests with real production log patterns • Improved gzip detection tests with actual compressed test data • Added integration tests for full log processing and concurrent operations • Updated .gitignore to allow testdata log files while excluding others • Updated TODO.md to reflect completed test infrastructure improvements * fix: comprehensive security hardening and critical bug fixes Security Enhancements: - Add command injection protection with allowlist validation for all external commands - Add security documentation to gzip functions warning about path traversal risks - Complete TODO.md security audit - all critical vulnerabilities addressed Bug Fixes: - Fix negative index access vulnerability in parallel operations (prevent panic) - Fix parsing inconsistency between BannedIn and BannedInWithContext functions - Fix nil error handling in concurrent log reading tests - Fix benchmark error simulation to measure actual performance vs error paths Implementation Details: - Add ValidateCommand() with allowlist for fail2ban-client, fail2ban-regex, service, systemctl, sudo - Integrate command validation into all OSRunner methods before execution - Replace manual string parsing with ParseBracketedList() for consistency - Add bounds checking (index >= 0) to prevent negative array access - Replace nil error with descriptive error message in concurrent error channels - Update banFunc in benchmark to return success instead of permanent errors Test Coverage: - Add comprehensive security validation tests with injection attempt patterns - Add parallel operations safety tests with index validation - Add parsing consistency tests between context/non-context functions - Add error handling demonstration tests for concurrent operations - Add gzip function security requirement documentation tests * perf: implement ultra-optimized log and ban record parsing with significant performance gains Major performance improvements to core fail2ban processing with comprehensive benchmarking: Performance Achievements: • Ban record parsing: 15% faster, 39% less memory, 45% fewer allocations • Log processing: 27% faster, 64% less memory, 32% fewer allocations • Cache performance: 624x faster cache hits with zero allocations • String pooling: 4.7x improvement with zero memory allocations Core Optimizations: • Object pooling (sync.Pool) for string slices, scanner buffers, and line buffers • Comprehensive caching (sync.Map) for gzip detection, file info, and path patterns • Fast path optimizations with extension-based gzip detection • Byte-level operations to reduce string allocations in filtering • Ultra-optimized parsers with smart field parsing and efficient time handling New Files: • fail2ban/ban_record_parser_optimized.go - High-performance ban record parser • fail2ban/log_performance_optimized.go - Ultra-optimized log processor with caching • fail2ban/ban_record_parser_benchmark_test.go - Ban record parsing benchmarks • fail2ban/log_performance_benchmark_test.go - Log performance benchmarks • fail2ban/ban_record_parser_compatibility_test.go - Compatibility verification tests Updated: • fail2ban/fail2ban.go - Integration with ultra-optimized parsers • TODO.md - Marked performance optimization tasks as completed * fix(ci): install dev dependencies for pre-commit * refactor: streamline pre-commit config and extract test helpers - Replace local hooks with upstream pre-commit repositories for better maintainability - Add new hooks: shellcheck, shfmt, checkov for enhanced code quality - Extract common test helpers into dedicated test_helpers.go to reduce duplication - Add warning logs for unreadable log files in fail2ban and logs packages - Remove hard-coded GID checks in sudo.go for better cross-platform portability - Update golangci-lint installation method in Makefile * fix(security): path traversal, log file validation * feat: complete pre-release modernization with comprehensive testing - Remove all deprecated legacy functions and dead code paths - Add security hardening with sanitized error messages - Implement comprehensive performance benchmarks and security audit tests - Mark all pre-release modernization tasks as completed (10/10) - Update project documentation to reflect full completion status * fix(ci): linting, and update gosec install source * feat: implement comprehensive test framework with 60-70% code reduction Major test infrastructure modernization: - Create fluent CommandTestBuilder framework for streamlined test creation - Add MockClientBuilder pattern for advanced mock configuration - Standardize table test field naming (expectedOut→wantOutput, expectError→wantError) - Consolidate test code: 3,796 insertions, 3,104 deletions (net +692 lines with enhanced functionality) Framework achievements: - 168+ tests passing with zero regressions - 5 cmd test files fully migrated to new framework - 63 field name standardizations applied - Advanced mock patterns with fluent interface File organization improvements: - Rename all test files with consistent prefixes (cmd_*, fail2ban_*, main_*) - Split monolithic test files into focused, maintainable modules - Eliminate cmd_test.go (622 lines) and main_test.go (825 lines) - Create specialized test files for better organization Documentation enhancements: - Update docs/testing.md with complete framework documentation - Optimize TODO.md from 231→72 lines (69% token reduction) - Add comprehensive migration guides and best practices Test framework components: - command_test_framework.go: Core fluent interface implementation - MockClientBuilder: Advanced mock configuration with builder pattern - table_test_standards.go: Standardized field naming conventions - Enhanced test helpers with error checking consolidation * chore: fixes, .go-version, linting * fix(ci) editorconfig in .pre-commit-config.yaml * fix: too broad gitignore * chore: update fail2ban/fail2ban_path_security_test.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Ismo Vuorinen <ismo@ivuorinen.net> * chore: code review fixes * chore: code review fixes * fix: more code review fixes * fix: more code review fixes * feat: cleanup, fixes, testing * chore: minor config file updates - Add quotes to F2B_TIMEOUT value in .env.example for clarity - Remove testdata log exception from .gitignore (simplified) * feat: implement comprehensive monitoring with structured logging and metrics - Add structured logging with context propagation throughout codebase - Implement ContextualLogger with request tracking and operation timing - Add context values for operation, IP, jail, command, and request ID - Integrate with existing logrus logging infrastructure - Add request/response timing metrics collection - Create comprehensive Metrics system with atomic counters - Track command executions, ban/unban operations, and client operations - Implement latency distribution buckets for performance analysis - Add validation cache hit/miss tracking - Enhance ban/unban commands with structured logging - Add LogOperation wrapper for automatic timing and context - Log individual jail operations with success/failure status - Integrate metrics recording with ban/unban operations - Add new 'metrics' command to expose collected metrics - Support both plain text and JSON output formats - Display system metrics (uptime, memory, goroutines) - Show operation counts, failures, and average latencies - Include latency distribution histograms - Update test infrastructure - Add tests for metrics command - Fix test helper to support persistent flags - Ensure all tests pass with new logging This completes the high-priority performance monitoring and structured logging requirements from TODO.md, providing comprehensive operational visibility into the f2b application. * docs: update TODO.md to reflect completed monitoring work - Mark structured logging and timing metrics as completed - Update test coverage stats (cmd/ improved from 66.4% to 76.8%) - Add completed infrastructure section for today's work - Update current status date and add monitoring to health indicators * feat: complete TODO.md technical debt cleanup Complete all remaining TODO.md tasks with comprehensive implementation: ## 🎯 Validation Caching Implementation - Thread-safe validation cache with sync.RWMutex protection - MetricsRecorder interface to avoid circular dependencies - Cached validation for IP, jail, filter, and command validation - Integration with existing metrics system for cache hit/miss tracking - 100% test coverage for caching functionality ## 🔧 Constants Extraction - Fail2Ban status codes: Fail2BanStatusSuccess, Fail2BanStatusAlreadyProcessed - Command constants: Fail2BanClientCommand, Fail2BanRegexCommand, Fail2BanServerCommand - File permissions: DefaultFilePermissions (0600), DefaultDirectoryPermissions (0750) - Timeout limits: MaxCommandTimeout, MaxFileTimeout, MaxParallelTimeout - Updated all references throughout codebase to use named constants ## 📊 Test Coverage Improvement - Increased fail2ban package coverage from 62.0% to 70.3% (target: 70%+) - Added 6 new comprehensive test files with 200+ additional test cases - Coverage improvements across all major components: - Context helpers, validation cache, mock clients, OS runner methods - Error constructors, timing operations, cache statistics - Thread safety and concurrency testing ## 🛠️ Code Quality & Fixes - Fixed all linting issues (golangci-lint, revive, errcheck) - Resolved unused parameter warnings and error handling - Fixed timing-dependent test failures in worker pool cancellation - Enhanced thread safety in validation caching ## 📈 Final Metrics - Overall test coverage: 72.4% (up from ~65%) - fail2ban package: 70.3% (exceeds 70% target) - cmd package: 76.9% - Zero TODO/FIXME/HACK comments in production code - 100% linting compliance * fix: resolve test framework issues and update documentation - Remove unnecessary defer/recover block in comprehensive_framework_test.go - Fix compilation error in command_test_framework.go variable redeclaration - Update TODO.md to reflect all 12 completed code quality fixes - Clean up dead code and improve test maintainability - Fix linting issues: error handling, code complexity, security warnings - Break down complex test function to reduce cyclomatic complexity * fix: replace dangerous test commands with safe placeholders Replaces actual dangerous commands in test cases with safe placeholder patterns to prevent accidental execution while maintaining comprehensive security testing. - Replace 'rm -rf /', 'cat /etc/passwd' with 'DANGEROUS_RM_COMMAND', 'DANGEROUS_SYSTEM_CALL' - Update GetDangerousCommandPatterns() to recognize both old and new patterns - Enhance filter validation with command injection protection (semicolons, pipes, backticks, dollar signs) - Add package documentation comments for all packages (main, cmd, fail2ban) - Fix GoReleaser static linking configuration for cross-platform builds - Remove Docker platform restriction to enable multi-arch support - Apply code formatting and linting fixes All security validation tests continue to pass with the safe placeholders. * fix: resolve TestMixedConcurrentOperations race condition and command key mismatches The concurrency test was failing due to several issues: 1. **Command Key Mismatch**: Test setup used "sudo test arg" key but MockRunner looked for "test arg" because "test" command doesn't require sudo 2. **Invalid Commands**: Using "test" and "echo" commands that aren't in the fail2ban command allowlist, causing validation failures 3. **Race Conditions**: Multiple goroutines setting different MockRunners simultaneously, overwriting responses **Solution:** - Replace invalid test commands ("test", "echo") with valid fail2ban commands ("fail2ban-client status", "fail2ban-client -V") - Pre-configure shared MockRunner with all required response keys for both sudo and non-sudo execution paths - Improve test structure to reduce race conditions between setup and execution All tests now pass reliably, resolving the CI failure. * fix: address code quality issues and improve test coverage - Replace unsafe type assertion with comma-ok idiom in logging - Fix TestTestFilter to use created filter instead of nonexistent - Add warning logs for invalid log level configurations - Update TestVersionCommand to use consistent test framework pattern - Remove unused LoggerContextKey constant - Add version command support to test framework - Fix trailing whitespace in test files * feat: add timeout handling and multi-architecture Docker support * test: enhance path traversal security test coverage * chore: comprehensive documentation update and linting fixes Updated all documentation to reflect current capabilities including context-aware operations, multi-architecture Docker support, advanced security features, and performance monitoring. Removed unused functions and fixed all linting issues. * fix(lint): .goreleaser.yaml * feat: add markdown link checker and fix all linting issues - Add markdown-link-check to pre-commit hooks with comprehensive configuration - Fix GitHub workflow structure (sync-labels.yml) with proper job setup - Add JSON schemas to all configuration files for better IDE support - Update tool installation in Makefile for markdown-link-check dependency - Fix all revive linting issues (Boolean literals, defer in loop, if-else simplification, method naming) - Resolve broken relative link in CONTRIBUTING.md - Configure rate limiting and ignore patterns for GitHub URLs - Enhance CLAUDE.md with link checking documentation * fix(ci): sync-labels permissions * docs: comprehensive documentation update reflecting current project status - Updated TODO.md to show production-ready status with 21 commands - Enhanced README.md with enterprise-grade features and capabilities - Added performance monitoring and timeout configuration to FAQ - Updated CLAUDE.md with accurate project architecture overview - Fixed all line length issues to meet EditorConfig requirements - Added .mega-linter.yml configuration for enhanced linting * fix: address CodeRabbitAI review feedback - Split .goreleaser.yaml builds for static/dynamic linking by architecture - Update docs to accurately reflect 7 path traversal patterns (not 17) - Fix containsPathTraversal to allow valid absolute paths - Replace runnerCombinedRunWithSudoContext with RunnerCombinedOutputWithSudoContext - Fix ldflags to use uppercase Version variable name - Remove duplicate test coverage metrics in TODO.md - Fix .markdown-link-check.json schema violations - Add v8r JSON validator to pre-commit hooks * chore(ci): update workflows, switch v8r to check-jsonschema * fix: restrict static linking to amd64 only in .goreleaser.yaml - Move arm64 from static to dynamic build configuration - Static linking now only applies to linux/amd64 - Prevents build failures due to missing static libc on ARM64 - All architectures remain supported with appropriate linking * fix(ci): caching * fix(ci): python caching with pip, node with npm * fix(ci): no caching for node then * fix(ci): no requirements.txt, no cache * refactor: address code review feedback - Pin Alpine base image to v3.20 for reproducible builds - Remove redundant --platform flags in GoReleaser Docker configs - Fix unused parameters in concurrency test goroutines - Simplify string search helper using strings.Contains() - Remove redundant error checking logic in security tests --------- Signed-off-by: Ismo Vuorinen <ismo@ivuorinen.net> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
24 KiB
Testing Guide
Testing Philosophy
f2b follows a comprehensive testing strategy that prioritizes security, reliability, and maintainability. The core principle is mock everything to ensure tests are fast, reliable, and never execute real system commands.
Our testing approach includes a modern fluent testing framework that reduces test code duplication by 60-70% while maintaining full functionality and improving readability. Enhanced with context-aware testing patterns, sophisticated security test coverage including 17 path traversal attack vectors, and thread-safe operations for comprehensive concurrent testing scenarios.
Test Organization
File Structure
- Unit tests: Co-located with source files using
*_test.gosuffix - Integration tests: Named
integration_test.gofor end-to-end scenarios - Test helpers: Shared utilities in test files
- Mocks: Comprehensive mock implementations in
fail2ban/package
Package Organization
cmd/
├── ban_test.go # Unit tests for ban command with context support
├── cmd_test.go # Shared test utilities and fluent framework
├── integration_test.go # End-to-end command tests with timeout handling
├── metrics_test.go # Performance metrics testing
├── parallel_operations_test.go # Concurrent operation testing
└── ...
fail2ban/
├── client_test.go # Client interface tests with context support
├── client_security_test.go # 17 path traversal security test cases
├── mock.go # Thread-safe MockClient implementation
├── mock_test.go # Mock behavior tests
├── concurrency_test.go # Thread safety and race condition tests
├── validation_cache_test.go # Caching system tests
└── ...
Testing Framework
Modern Fluent Interface (RECOMMENDED)
f2b provides a modern fluent testing framework that dramatically reduces test code duplication:
Basic Usage
// Simple command test (replaces 10+ lines with 4)
NewCommandTest(t, "ban").
WithArgs("192.168.1.100", "sshd").
ExpectSuccess().
Run()
// Error testing with context support
NewCommandTest(t, "ban").
WithArgs("invalid-ip", "sshd").
WithContext(context.WithTimeout(context.Background(), time.Second*5)).
ExpectError().
Run().
AssertContains("invalid IP address")
// JSON output validation with timeout handling
NewCommandTest(t, "banned").
WithArgs("sshd").
WithJSONFormat().
WithContext(context.WithTimeout(context.Background(), time.Second*10)).
ExpectSuccess().
Run().
AssertJSONField("Jail", "sshd")
// Parallel operation testing
NewCommandTest(t, "banned").
WithArgs("all").
WithParallelExecution(true).
ExpectSuccess().
Run().
AssertNotEmpty()
Advanced Framework Features with Context Support
// Environment setup with automatic cleanup and context support
env := NewTestEnvironment().
WithPrivileges(true).
WithMockRunner().
WithContextTimeout(time.Second*30)
defer env.Cleanup()
// Complex test with chained assertions and timeout handling
result := NewCommandTest(t, "status").
WithArgs("sshd").
WithEnvironment(env).
WithContext(context.WithTimeout(context.Background(), time.Second*10)).
WithSetup(func(mock *fail2ban.MockClient) {
setMockJails(mock, []string{"sshd", "apache"})
mock.StatusJailData = map[string]string{
"sshd": "Status for sshd jail",
}
// Configure context-aware operations
mock.EnableContextSupport = true
}).
ExpectSuccess().
Run()
// Multiple validations on same result with performance metrics
result.AssertContains("Status for sshd").
AssertNotContains("apache").
AssertNotEmpty().
AssertExecutionTime(time.Millisecond*100) // Performance assertion
// Concurrent operation testing
result := NewCommandTest(t, "banned").
WithArgs("all").
WithConcurrentWorkers(4).
WithSetup(func(mock *fail2ban.MockClient) {
// Setup thread-safe mock operations
mock.EnableConcurrentAccess = true
setMockJails(mock, []string{"sshd", "apache", "nginx"})
}).
ExpectSuccess().
Run().
AssertConcurrentSafety()
Mock Client Builder Pattern (Advanced Configuration)
The framework includes a fluent MockClientBuilder for complex mock scenarios:
// Advanced mock setup with builder pattern and context support
mockBuilder := NewMockClientBuilder().
WithJails("sshd", "apache").
WithBannedIP("192.168.1.100", "sshd").
WithBanRecord("sshd", "192.168.1.100", "01:30:00").
WithLogLine("2024-01-01 12:00:00 [sshd] Ban 192.168.1.100").
WithStatusResponse("sshd", "Mock status for jail sshd").
WithBanError("apache", "192.168.1.101", errors.New("ban failed")).
WithContextSupport(true).
WithValidationCache(true).
WithParallelProcessing(true)
// Use builder in test with context and performance monitoring
NewCommandTest(t, "banned").
WithArgs("sshd").
WithMockBuilder(mockBuilder).
WithContext(context.WithTimeout(context.Background(), time.Second*5)).
WithMetricsCollection(true).
ExpectSuccess().
ExpectOutput("sshd | 192.168.1.100").
AssertExecutionTime(time.Millisecond*50).
Run()
Builder Methods
WithJails(jails...)- Configure available jailsWithBannedIP(ip, jail)- Add banned IP to jailWithBanRecord(jail, ip, remaining)- Add ban record with timeWithLogLine(line)- Add log entryWithStatusResponse(jail, response)- Configure status responsesWithBanError(jail, ip, err)- Configure ban operation errorsWithUnbanError(jail, ip, err)- Configure unban operation errorsWithContextSupport(bool)- Enable context-aware operationsWithValidationCache(bool)- Enable validation cachingWithParallelProcessing(bool)- Enable concurrent operationsWithTimeoutHandling(duration)- Configure timeout behaviorWithSecurityTesting(bool)- Enable security test patternsWithPathTraversalProtection(bool)- Enable path traversal test coverage
Table-Driven Tests with Framework
Standardized Field Naming: f2b uses consistent field naming conventions across all table-driven tests:
func TestCommandsWithFramework(t *testing.T) {
tests := []struct {
name string // Test case name - REQUIRED
command string // Command to test
args []string // Command arguments
wantError bool // Whether error is expected (not expectError)
wantOutput string // Expected output content (not expectedOut/expectedOutput)
wantErrorMsg string // Specific error message (not expectedError)
}{
{"ban_success", "ban", []string{"192.168.1.100", "sshd"}, false, "Banned", ""},
{"invalid_jail", "ban", []string{"192.168.1.100", "invalid"}, true, "", "not found"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
builder := NewCommandTest(t, tt.command).
WithArgs(tt.args...)
if tt.wantError {
builder = builder.ExpectError()
} else {
builder = builder.ExpectSuccess()
}
if tt.wantOutput != "" {
builder.ExpectOutput(tt.wantOutput)
}
builder.Run()
})
}
}
Standardized Field Naming Conventions
✅ Consistent Patterns:
wantOutput- Expected output contentwantError- Whether error is expectedwantErrorMsg- Specific error message to check
This standardization improves code maintainability and aligns with Go testing conventions.
Framework Benefits
✅ Production Results:
- 60-70% less code: Fluent interface reduces boilerplate
- 168+ tests passing: All tests converted successfully maintain functionality
- 5 files standardized: Complete migration of cmd test files
- 63 field name standardizations: Consistent naming across all table tests
Key Improvements:
- Consistent patterns: Standardized across all tests
- Better readability: Self-documenting test intentions
- Powerful assertions: Built-in JSON, error, and output validation
- Environment management: Automated setup and cleanup
- Advanced mock patterns: MockClientBuilder for complex scenarios
- Backward compatible: Works alongside existing test patterns
File-Specific Achievements:
cmd_commands_test.go: 529 lines (reduced from 780)cmd_service_test.go: 284 lines (reduced from 640)cmd_integration_test.go: 182 lines (reduced from 223)cmd_root_test.go: Completion and execute tests standardizedcmd_logswatch_test.go: Logs watch tests standardized
Framework Example
The modern testing framework provides a clean, fluent interface:
// Modern framework approach
NewCommandTest(t, "status").
WithArgs("all").
WithSetup(func(mock *fail2ban.MockClient) {
setMockJails(mock, []string{"sshd"})
mock.StatusAllData = "Status for all jails"
}).
ExpectSuccess().
ExpectOutput("Status for all jails").
Run()
This approach provides excellent readability and reduced boilerplate.
Mock Patterns
MockClient Usage
The MockClient is a comprehensive, thread-safe mock implementation of the Client interface:
// Basic setup
mock := fail2ban.NewMockClient()
mock.Jails = map[string]struct{}{"sshd": {}, "apache": {}}
// Configure responses
mock.StatusAllData = "Jail list: sshd apache"
mock.StatusJailData = map[string]string{
"sshd": "Status for sshd jail",
}
// Set up banned IPs
mock.Banned = map[string]map[string]time.Time{
"sshd": {"192.168.1.100": time.Now()},
}
MockRunner Setup
For testing command execution:
// Save original and set up mock
mockRunner := fail2ban.NewMockRunner()
originalRunner := fail2ban.GetRunner()
defer fail2ban.SetRunner(originalRunner)
fail2ban.SetRunner(mockRunner)
// Configure command responses
mockRunner.SetResponse("fail2ban-client status", []byte("Jail list: sshd"))
MockSudoChecker Pattern
For testing privilege scenarios:
// Modern standardized setup with automatic cleanup
_, cleanup := fail2ban.SetupMockEnvironmentWithSudo(t, true)
defer cleanup()
// The mock environment is now fully configured with privileges
Testing Requirements
Advanced Security Testing
- Never execute real sudo commands - Always use
MockSudoCheckerandMockRunner - Test both privilege paths - Include tests for privileged and unprivileged users with context support
- Validate input sanitization - Test with malicious inputs including 17 path traversal attack vectors
- Test privilege escalation - Ensure commands escalate only when necessary with timeout protection
- Context-aware security testing - Test timeout and cancellation behavior in security scenarios
- Thread-safe security operations - Test concurrent access to security-critical functions
- Performance security testing - Test DoS protection through validation caching
- Advanced path traversal protection - Test Unicode normalization, mixed case, and Windows-style attacks
Test Environment Setup
func TestWithMocks(t *testing.T) {
// Modern standardized setup with automatic cleanup and context support
_, cleanup := fail2ban.SetupMockEnvironmentWithSudo(t, true)
defer cleanup()
// Create context for timeout testing
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
// All mock environment is configured with:
// - Thread-safe operations
// - Context-aware timeout handling
// - Validation caching enabled
// - Security test coverage patterns
// - Performance metrics collection
}
Common Test Scenarios
Testing Commands with Privileges
func TestBanCommand_RequiresPrivileges(t *testing.T) {
tests := []struct {
name string
hasPrivileges bool
expectError bool
timeout time.Duration
}{
{"with privileges", true, false, time.Second*5},
{"without privileges", false, true, time.Second*5},
{"with privileges timeout", true, false, time.Millisecond*100},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set up privilege scenario using modern helper with context support
_, cleanup := fail2ban.SetupMockEnvironmentWithSudo(t, tt.hasPrivileges)
defer cleanup()
// Create context with timeout for the test
ctx, cancel := context.WithTimeout(context.Background(), tt.timeout)
defer cancel()
// Test command execution with context support
client := fail2ban.GetClient()
err := client.BanIPWithContext(ctx, "192.168.1.100", "sshd")
if (err != nil) != tt.expectError {
t.Errorf("BanIPWithContext() error = %v, expectError %v", err, tt.expectError)
}
// Test context cancellation behavior
if ctx.Err() != nil {
t.Logf("Context cancelled as expected: %v", ctx.Err())
}
})
}
}
Advanced Security Input Validation Testing
func TestValidateIP_AdvancedSecurityChecks(t *testing.T) {
tests := []struct {
name string
ip string
wantErr bool
attackType string
}{
{"valid IPv4", "192.168.1.1", false, ""},
{"valid IPv6", "2001:db8::1", false, ""},
{"invalid IP", "not-an-ip", true, "basic"},
{"malicious input", "192.168.1.1; rm -rf /", true, "command_injection"},
{"basic path traversal", "../../../etc/passwd", true, "path_traversal"},
{"url encoded traversal", "%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd", true, "url_encoding"},
{"null byte injection", "192.168.1.1\x00/../../../etc/passwd", true, "null_byte"},
{"unicode normalization", "/var/log/\u002e\u002e/\u002e\u002e/etc/passwd", true, "unicode_attack"},
{"mixed case traversal", "/var/LOG/../../../etc/passwd", true, "mixed_case"},
{"multiple slashes", "/var/log////../../etc/passwd", true, "multiple_slashes"},
{"windows style", "/var/log\\..\\..\\..\etc\passwd", true, "windows_style"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test with validation caching enabled
start := time.Now()
err := fail2ban.ValidateIP(tt.ip)
duration := time.Since(start)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateIP() error = %v, wantErr %v", err, tt.wantErr)
}
// Test caching performance on second call
if !tt.wantErr {
start2 := time.Now()
err2 := fail2ban.ValidateIP(tt.ip)
duration2 := time.Since(start2)
if err2 != nil {
t.Errorf("Cached validation failed: %v", err2)
}
// Second call should be faster due to caching
if duration2 > duration {
t.Logf("Cache may not be working: first=%v, second=%v", duration, duration2)
}
}
// Log attack type for security analysis
if tt.attackType != "" {
t.Logf("Successfully blocked %s attack: %s", tt.attackType, tt.ip)
}
})
}
}
// Test concurrent validation safety
func TestValidateIP_ConcurrentSafety(t *testing.T) {
testIPs := []string{
"192.168.1.1",
"192.168.1.2",
"invalid-ip",
"../../../etc/passwd",
}
var wg sync.WaitGroup
results := make(chan error, len(testIPs)*10)
// Test concurrent validation calls
for i := 0; i < 10; i++ {
for _, ip := range testIPs {
wg.Add(1)
go func(testIP string) {
defer wg.Done()
err := fail2ban.ValidateIP(testIP)
results <- err
}(ip)
}
}
wg.Wait()
close(results)
// Verify no race conditions occurred
errorCount := 0
for err := range results {
if err != nil {
errorCount++
}
}
t.Logf("Concurrent validation completed with %d errors out of %d calls",
errorCount, len(testIPs)*10)
}
Testing Output Formats
func TestCommandOutput_JSONFormat(t *testing.T) {
mock := fail2ban.NewMockClient()
config := &cmd.Config{Format: "json"}
output, err := executeCommandWithConfig(mock, config, "banned", "all")
if err != nil {
t.Fatalf("Command failed: %v", err)
}
// Validate JSON output
var result []interface{}
if err := json.Unmarshal([]byte(output), &result); err != nil {
t.Errorf("Invalid JSON output: %v", err)
}
}
Integration Testing
End-to-End Command Testing
func TestIntegration_BanUnbanFlow(t *testing.T) {
// Modern setup with automatic cleanup
_, cleanup := fail2ban.SetupMockEnvironment(t)
defer cleanup()
// Get the configured mock client
mock := fail2ban.GetClient().(*fail2ban.MockClient)
// Test complete workflow
steps := []struct {
command []string
expectError bool
validate func(string) error
}{
{[]string{"ban", "192.168.1.100", "sshd"}, false, validateBanOutput},
{[]string{"test", "192.168.1.100"}, false, validateTestOutput},
{[]string{"unban", "192.168.1.100", "sshd"}, false, validateUnbanOutput},
}
for _, step := range steps {
output, err := executeCommand(mock, step.command...)
if (err != nil) != step.expectError {
t.Errorf("Command %v: error = %v, expectError = %v",
step.command, err, step.expectError)
}
if step.validate != nil {
if err := step.validate(output); err != nil {
t.Errorf("Validation failed for %v: %v", step.command, err)
}
}
}
}
Performance Testing
Benchmarking Critical Paths
func BenchmarkBanCommand(b *testing.B) {
// Modern setup for benchmarks
_, cleanup := fail2ban.SetupMockEnvironment(b)
defer cleanup()
mock := fail2ban.GetClient().(*fail2ban.MockClient)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := executeCommand(mock, "ban", "192.168.1.100", "sshd")
if err != nil {
b.Fatalf("Ban command failed: %v", err)
}
}
}
Test Coverage Requirements
Enhanced Coverage Requirements
- Overall: 85%+ test coverage across the codebase
- Security-critical code: 95%+ coverage for privilege handling with context support
- Command implementations: 90%+ coverage for all CLI commands including timeout scenarios
- Input validation: 100% coverage for validation functions including 17 path traversal cases
- Context operations: 90%+ coverage for timeout and cancellation behavior
- Concurrent operations: 85%+ coverage for thread-safe functions
- Performance features: 80%+ coverage for caching and metrics systems
Coverage Verification
# Run tests with coverage
go test -coverprofile=coverage.out ./...
# View coverage report
go tool cover -html=coverage.out
# Check coverage percentage
go tool cover -func=coverage.out | grep total
Common Testing Pitfalls
Avoid These Mistakes
- Real sudo execution in tests - Always use MockSudoChecker
- Hardcoded file paths - Use temporary files or mocks
- Network dependencies - Mock all external calls
- Race conditions - Use proper synchronization in concurrent tests
- Leaked goroutines - Clean up background processes
- Platform dependencies - Write portable tests
Enhanced Security Testing Checklist
- All privileged operations use mocks with context support
- Input validation tested with malicious inputs including 17 path traversal attack vectors
- Both privileged and unprivileged paths tested with timeout scenarios
- No real file system modifications
- No actual network calls
- Environment variables properly isolated
- Context-aware timeout behavior tested
- Thread-safe concurrent operations verified
- Validation caching security tested (DoS protection)
- Performance degradation attack scenarios covered
- Unicode normalization attacks tested
- Mixed case and Windows-style path attacks covered
Test Utilities
Modern Test Helpers (RECOMMENDED)
The framework provides standardized helpers that reduce duplication:
// Standardized error checking (replaces 6 lines with 1)
fail2ban.AssertError(t, err, expectError, testName)
// Command output validation
fail2ban.AssertCommandSuccess(t, err, output, expectedOutput, testName)
fail2ban.AssertCommandError(t, err, output, expectedError, testName)
// Environment setup with automatic cleanup
_, cleanup := fail2ban.SetupMockEnvironmentWithSudo(t, hasPrivileges)
defer cleanup()
Framework Components
CommandTestBuilder Methods
Basic Configuration:
WithArgs(args...)- Set command argumentsWithMockClient(mock)- Use specific mock clientWithMockBuilder(builder)- Use MockClientBuilder for advanced setupWithJSONFormat()- Enable JSON output testingWithSetup(func)- Configure mock clientWithEnvironment(env)- Use test environment
Expectations:
ExpectSuccess()/ExpectError()- Set error expectationsExpectOutput(text)- Validate output contains textExpectExactOutput(text)- Validate exact output match
Service Commands:
WithServiceSetup(response, error)- Configure service command mocks- Service commands support stdout/stderr capture automatically
CommandTestResult Assertions
AssertContains(text)- Output contains textAssertNotContains(text)- Output doesn't contain textAssertEmpty()/AssertNotEmpty()- Output emptinessAssertJSONField(path, value)- JSON field validationAssertExactOutput(text)- Exact output match
TestEnvironment Setup
WithPrivileges(bool)- Configure sudo privilegesWithMockRunner()- Set up command runner mocksWithStdoutCapture()- Capture stdout for validationCleanup()- Restore original environment
Standard Test Setup Example
// SetupMockEnvironmentWithSudo configures standard test environment with privileges
func TestExample(t *testing.T) {
_, cleanup := fail2ban.SetupMockEnvironmentWithSudo(t, true)
defer cleanup()
// Test implementation here
}
// executeCommand runs a command with mock client
func executeCommand(client fail2ban.Client, args ...string) (string, error) {
config := &cmd.Config{Format: "plain"}
root := cmd.NewRootCmd(client, config)
var output bytes.Buffer
root.SetOutput(&output)
root.SetArgs(args)
err := root.Execute()
return output.String(), err
}
Running Tests
Basic Test Execution
# Run all tests
go test ./...
# Run tests with verbose output
go test -v ./...
# Run specific test
go test -run TestBanCommand ./cmd
# Run tests with race detection
go test -race ./...
Enhanced Security-Focused Testing
# Run tests with sudo checking enabled
F2B_TEST_SUDO=true go test ./...
# Run comprehensive security tests including path traversal
go test -run "Security|Sudo|Privilege|PathTraversal|Context|Timeout" ./...
# Run concurrent safety tests
go test -run "Concurrent|Race|ThreadSafe" -race ./...
# Run performance security tests (caching, DoS protection)
go test -run "Cache|Performance|Validation" ./...
# Run advanced path traversal security tests
go test -run "PathTraversal|Unicode|Mixed|Windows" ./fail2ban
# Run context and timeout behavior tests
go test -run "Context|Timeout|Cancel" ./...
End-to-End Testing
# Run integration tests only
go test -run Integration ./cmd
# Run with coverage for integration tests
go test -coverprofile=integration.out -run Integration ./cmd
This comprehensive testing approach ensures f2b remains secure, reliable, and maintainable while providing confidence for all changes and contributions. The enhanced testing framework includes context-aware operations, sophisticated security coverage with 17 path traversal attack vectors, thread-safe concurrent testing, performance-oriented validation caching tests, and comprehensive timeout handling verification for enterprise-grade reliability.