mirror of
https://github.com/ivuorinen/f2b.git
synced 2026-01-26 03:13:58 +00:00
* refactor: consolidate test helpers and reduce code duplication
- Fix prealloc lint issue in cmd_logswatch_test.go
- Add validateIPAndJails helper to consolidate IP/jail validation
- Add WithTestRunner/WithTestSudoChecker helpers for cleaner test setup
- Replace setupBasicMockResponses duplicates with StandardMockSetup
- Add SetupStandardResponses/SetupJailResponses to MockRunner
- Delegate cmd context helpers to fail2ban implementations
- Document context wrapper pattern in context_helpers.go
* refactor: consolidate duplicate code patterns across cmd and fail2ban packages
Add helper functions to reduce code duplication found by dupl:
- safeCloseFile/safeCloseReader: centralize file close error logging
- createTimeoutContext: consolidate timeout context creation pattern
- withContextCheck: wrap context cancellation checks
- recordOperationMetrics: unify metrics recording for commands/clients
Also includes Phase 1 consolidations:
- copyBuckets helper for metrics snapshots
- Table-driven context extraction in logging
- processWithValidation helper for IP processors
* refactor: consolidate LoggerInterface by embedding LoggerEntry
Both interfaces had identical method signatures. LoggerInterface now
embeds LoggerEntry to eliminate code duplication.
* refactor: consolidate test framework helpers and fix test patterns
- Add checkJSONFieldValue and failMissingJSONField helpers to reduce
duplication in JSON assertion methods
- Add ParallelTimeout to default test config
- Fix test to use WithTestRunner inside test loop for proper mock scoping
* refactor: unify ban/unban operations with OperationType pattern
Introduce OperationType struct to consolidate duplicate ban/unban logic:
- Add ProcessOperation and ProcessOperationWithContext generic functions
- Add ProcessOperationParallel and ProcessOperationParallelWithContext
- Existing ProcessBan*/ProcessUnban* functions now delegate to generic versions
- Reduces ~120 lines of duplicate code between ban and unban operations
* refactor: consolidate time parsing cache pattern
Add ParseWithLayout method to BoundedTimeCache that consolidates the
cache-lookup-parse-store pattern. FastTimeCache and TimeParsingCache
now delegate to this method instead of duplicating the logic.
* refactor: consolidate command execution patterns in fail2ban
- Add validateCommandExecution helper for command/argument validation
- Add runWithTimerContext helper for timed runner operations
- Add executeIPActionWithContext to unify BanIP/UnbanIP implementations
- Reduces duplicate validation and execution boilerplate
* refactor: consolidate logrus adapter with embedded loggerCore
Introduce loggerCore type that provides the 8 standard logging methods
(Debug, Info, Warn, Error, Debugf, Infof, Warnf, Errorf). Both
logrusAdapter and logrusEntryAdapter now embed this type, eliminating
16 duplicate method implementations.
* refactor: consolidate path validation patterns
- Add validateConfigPathWithFallback helper in cmd/config_utils.go
for the validate-or-fallback-with-logging pattern
- Add validateClientPath helper in fail2ban/helpers.go for client
path validation delegation
* fix: add context cancellation checks to wrapper functions
- wrapWithContext0/1/2 now check ctx.Err() before invoking wrapped function
- WithCommand now validates and trims empty command strings
* refactor: extract formatLatencyBuckets for deterministic metrics output
Add formatLatencyBuckets helper that writes latency bucket distribution
with sorted keys for deterministic output, eliminating duplicate
formatting code for command and client latency buckets.
* refactor: add generic setNestedMapValue helper for mock configuration
Add setNestedMapValue[T] generic helper that consolidates the repeated
pattern of mutex-protected nested map initialization and value setting
used by SetBanError, SetBanResult, SetUnbanError, and SetUnbanResult.
* fix: use cmd.Context() for signal propagation and correct mock status
- ExecuteIPCommand now uses cmd.Context() instead of context.Background()
to inherit Cobra's signal cancellation
- MockRunner.SetupJailResponses uses shared.Fail2BanStatusSuccess ("0")
instead of literal "1" for proper success path simulation
* fix: restore operation-specific log messages in ProcessOperationWithContext
Add back Logger.WithFields().Info(opType.Message) call that was lost
during refactoring. This restores the distinction between ban and unban
operation messages (shared.MsgBanResult vs shared.MsgUnbanResult).
* fix: return aggregated errors from parallel operations
Previously, errors from individual parallel operations were silently
swallowed - converted to status strings but never returned to callers.
Now processOperations collects all errors and returns them aggregated
via errors.Join, allowing callers to distinguish partial failures from
complete success while still receiving all results.
* fix: add input validation to processOperations before parallel execution
Validate IP and jail inputs at the start of processOperations() using
fail2ban.CachedValidateIP and CachedValidateJail. This prevents invalid
or malicious inputs (empty values, path traversal attempts, malformed
IPs) from reaching the operation functions. All validation errors are
aggregated and returned before any operations execute.
602 lines
17 KiB
Go
602 lines
17 KiB
Go
// Package cmd provides a comprehensive testing framework for CLI commands.
|
|
// This package offers fluent testing utilities, mock builders, and standardized
|
|
// test patterns to ensure robust testing of f2b command functionality.
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/ivuorinen/f2b/shared"
|
|
|
|
"github.com/ivuorinen/f2b/fail2ban"
|
|
)
|
|
|
|
// CommandTestResult represents the result of a command execution
|
|
type CommandTestResult struct {
|
|
Output string
|
|
Error error
|
|
t *testing.T
|
|
name string
|
|
}
|
|
|
|
// CommandTestBuilder provides a fluent interface for testing commands
|
|
type CommandTestBuilder struct {
|
|
t *testing.T
|
|
name string
|
|
command string
|
|
args []string
|
|
mockClient *fail2ban.MockClient
|
|
config *Config
|
|
expectError bool
|
|
expectedOut string
|
|
exactMatch bool
|
|
setupFunc func(*fail2ban.MockClient)
|
|
environment *TestEnvironment
|
|
}
|
|
|
|
// TestEnvironment manages test environment setup and cleanup
|
|
type TestEnvironment struct {
|
|
originalChecker fail2ban.SudoChecker
|
|
originalRunner fail2ban.Runner
|
|
originalStdout *os.File
|
|
stdoutReader *os.File
|
|
stdoutWriter *os.File
|
|
cleanup []func()
|
|
}
|
|
|
|
// NewTestEnvironment creates a new test environment manager
|
|
func NewTestEnvironment() *TestEnvironment {
|
|
return &TestEnvironment{
|
|
cleanup: make([]func(), 0),
|
|
}
|
|
}
|
|
|
|
// WithPrivileges sets up sudo checker with specified privileges
|
|
func (env *TestEnvironment) WithPrivileges(hasPrivileges bool) *TestEnvironment {
|
|
env.originalChecker = fail2ban.GetSudoChecker()
|
|
mockChecker := &fail2ban.MockSudoChecker{
|
|
MockHasPrivileges: hasPrivileges,
|
|
ExplicitPrivilegesSet: true,
|
|
}
|
|
fail2ban.SetSudoChecker(mockChecker)
|
|
env.cleanup = append(env.cleanup, func() {
|
|
fail2ban.SetSudoChecker(env.originalChecker)
|
|
})
|
|
return env
|
|
}
|
|
|
|
// WithMockRunner sets up a mock runner with common responses
|
|
func (env *TestEnvironment) WithMockRunner() *TestEnvironment {
|
|
env.originalRunner = fail2ban.GetRunner()
|
|
mockRunner := fail2ban.NewMockRunner()
|
|
// Set up common responses
|
|
mockRunner.SetResponse(shared.MockCommandVersion, []byte(shared.VersionOutput))
|
|
mockRunner.SetResponse(shared.MockCommandPing, []byte(shared.PingOutput))
|
|
mockRunner.SetResponse(shared.MockCommandStatus, []byte(shared.StatusOutput))
|
|
mockRunner.SetResponse("sudo service fail2ban status", []byte("● fail2ban.service - Fail2Ban Service"))
|
|
fail2ban.SetRunner(mockRunner)
|
|
|
|
env.cleanup = append(env.cleanup, func() {
|
|
fail2ban.SetRunner(env.originalRunner)
|
|
})
|
|
return env
|
|
}
|
|
|
|
// WithStdoutCapture captures stdout for testing output
|
|
func (env *TestEnvironment) WithStdoutCapture() *TestEnvironment {
|
|
env.originalStdout = os.Stdout
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
// Return early with nil fields to indicate failure
|
|
return env
|
|
}
|
|
env.stdoutReader = r
|
|
env.stdoutWriter = w
|
|
os.Stdout = w
|
|
|
|
env.cleanup = append(env.cleanup, func() {
|
|
os.Stdout = env.originalStdout
|
|
if env.stdoutWriter != nil {
|
|
_ = env.stdoutWriter.Close()
|
|
}
|
|
if env.stdoutReader != nil {
|
|
_ = env.stdoutReader.Close()
|
|
}
|
|
})
|
|
return env
|
|
}
|
|
|
|
// Cleanup restores the original environment
|
|
func (env *TestEnvironment) Cleanup() {
|
|
for i := len(env.cleanup) - 1; i >= 0; i-- {
|
|
env.cleanup[i]()
|
|
}
|
|
}
|
|
|
|
// ReadStdout reads the captured stdout content
|
|
func (env *TestEnvironment) ReadStdout() string {
|
|
if env.stdoutWriter == nil || env.stdoutReader == nil {
|
|
return ""
|
|
}
|
|
|
|
// Close writer if not already closed
|
|
if env.stdoutWriter != nil {
|
|
_ = env.stdoutWriter.Close()
|
|
env.stdoutWriter = nil // Prevent multiple closures
|
|
}
|
|
|
|
// Use io.ReadAll for dynamic buffer reading
|
|
if data, err := io.ReadAll(env.stdoutReader); err == nil {
|
|
return string(data)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// NewCommandTest creates a new command test builder
|
|
func NewCommandTest(t *testing.T, commandName string) *CommandTestBuilder {
|
|
t.Helper()
|
|
return &CommandTestBuilder{
|
|
t: t,
|
|
name: commandName,
|
|
command: commandName,
|
|
args: make([]string, 0),
|
|
config: &Config{
|
|
Format: PlainFormat,
|
|
CommandTimeout: shared.DefaultCommandTimeout,
|
|
FileTimeout: shared.DefaultFileTimeout,
|
|
ParallelTimeout: shared.DefaultParallelTimeout,
|
|
},
|
|
}
|
|
}
|
|
|
|
// WithName sets the test name for better error reporting
|
|
func (ctb *CommandTestBuilder) WithName(name string) *CommandTestBuilder {
|
|
ctb.name = name
|
|
return ctb
|
|
}
|
|
|
|
// WithArgs sets the command arguments
|
|
func (ctb *CommandTestBuilder) WithArgs(args ...string) *CommandTestBuilder {
|
|
ctb.args = args
|
|
return ctb
|
|
}
|
|
|
|
// WithMockClient sets the mock client for the test
|
|
func (ctb *CommandTestBuilder) WithMockClient(mock *fail2ban.MockClient) *CommandTestBuilder {
|
|
ctb.mockClient = mock
|
|
return ctb
|
|
}
|
|
|
|
// WithJSONFormat sets the output format to JSON
|
|
func (ctb *CommandTestBuilder) WithJSONFormat() *CommandTestBuilder {
|
|
if ctb.config == nil {
|
|
ctb.config = &Config{}
|
|
}
|
|
ctb.config.Format = JSONFormat
|
|
return ctb
|
|
}
|
|
|
|
// WithSetup provides a function to set up the mock client with specific data
|
|
func (ctb *CommandTestBuilder) WithSetup(setupFunc func(*fail2ban.MockClient)) *CommandTestBuilder {
|
|
ctb.setupFunc = setupFunc
|
|
return ctb
|
|
}
|
|
|
|
// WithServiceSetup provides a function to set up mock runner for service commands
|
|
func (ctb *CommandTestBuilder) WithServiceSetup(setupFunc func(*fail2ban.MockRunner)) *CommandTestBuilder {
|
|
ctb.setupFunc = func(_ *fail2ban.MockClient) {
|
|
// Set up sudo checker
|
|
mockChecker := &fail2ban.MockSudoChecker{
|
|
MockHasPrivileges: true,
|
|
ExplicitPrivilegesSet: true,
|
|
}
|
|
fail2ban.SetSudoChecker(mockChecker)
|
|
|
|
// Create and set up mock runner
|
|
mockRunner := &fail2ban.MockRunner{
|
|
Responses: make(map[string][]byte),
|
|
Errors: make(map[string]error),
|
|
}
|
|
setupFunc(mockRunner)
|
|
fail2ban.SetRunner(mockRunner)
|
|
}
|
|
return ctb
|
|
}
|
|
|
|
// WithEnvironment sets the test environment
|
|
func (ctb *CommandTestBuilder) WithEnvironment(env *TestEnvironment) *CommandTestBuilder {
|
|
ctb.environment = env
|
|
return ctb
|
|
}
|
|
|
|
// ExpectError indicates that the command should fail
|
|
func (ctb *CommandTestBuilder) ExpectError() *CommandTestBuilder {
|
|
ctb.expectError = true
|
|
return ctb
|
|
}
|
|
|
|
// ExpectSuccess indicates that the command should succeed
|
|
func (ctb *CommandTestBuilder) ExpectSuccess() *CommandTestBuilder {
|
|
ctb.expectError = false
|
|
return ctb
|
|
}
|
|
|
|
// ExpectOutput sets the expected output substring
|
|
func (ctb *CommandTestBuilder) ExpectOutput(expectedOut string) *CommandTestBuilder {
|
|
ctb.expectedOut = expectedOut
|
|
return ctb
|
|
}
|
|
|
|
// ExpectExactOutput sets the expected output for exact matching
|
|
func (ctb *CommandTestBuilder) ExpectExactOutput(expectedOut string) *CommandTestBuilder {
|
|
ctb.expectedOut = expectedOut
|
|
ctb.exactMatch = true
|
|
return ctb
|
|
}
|
|
|
|
// Run executes the command test and performs all validations
|
|
func (ctb *CommandTestBuilder) Run() *CommandTestResult {
|
|
ctb.t.Helper()
|
|
|
|
// Set up default mock client if none provided
|
|
if ctb.mockClient == nil {
|
|
ctb.mockClient = fail2ban.NewMockClient()
|
|
}
|
|
|
|
// Apply setup function if provided
|
|
if ctb.setupFunc != nil {
|
|
ctb.setupFunc(ctb.mockClient)
|
|
}
|
|
|
|
// Execute the command
|
|
output, err := ctb.executeCommand()
|
|
|
|
// Create result
|
|
result := &CommandTestResult{
|
|
Output: output,
|
|
Error: err,
|
|
t: ctb.t,
|
|
name: ctb.name,
|
|
}
|
|
|
|
// Perform basic validations
|
|
result.AssertError(ctb.expectError)
|
|
|
|
if ctb.expectedOut != "" {
|
|
if ctb.exactMatch {
|
|
result.AssertExactOutput(ctb.expectedOut)
|
|
} else {
|
|
result.AssertContains(ctb.expectedOut)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// executeCommand runs the actual command with the configured parameters
|
|
func (ctb *CommandTestBuilder) executeCommand() (string, error) {
|
|
var cmd *cobra.Command
|
|
|
|
switch ctb.command {
|
|
case "ban":
|
|
cmd = BanCmd(ctb.mockClient, ctb.config)
|
|
case "unban":
|
|
cmd = UnbanCmd(ctb.mockClient, ctb.config)
|
|
case "status":
|
|
cmd = StatusCmd(ctb.mockClient, ctb.config)
|
|
case shared.CLICmdListJails:
|
|
cmd = ListJailsCmd(ctb.mockClient, ctb.config)
|
|
case "banned":
|
|
cmd = BannedCmd(ctb.mockClient, ctb.config)
|
|
case "test":
|
|
cmd = TestIPCmd(ctb.mockClient, ctb.config)
|
|
case "logs":
|
|
cmd = LogsCmd(ctb.mockClient, ctb.config)
|
|
case shared.ServiceCommand:
|
|
cmd = ServiceCmd(ctb.config)
|
|
case shared.CLICmdVersion:
|
|
cmd = VersionCmd(ctb.config)
|
|
default:
|
|
return "", fmt.Errorf("unknown command: %s", ctb.command)
|
|
}
|
|
|
|
// For service commands, we need to capture os.Stdout since PrintOutput writes directly to it
|
|
if ctb.command == shared.ServiceCommand {
|
|
return ctb.executeServiceCommand(cmd)
|
|
}
|
|
|
|
// Execute regular commands
|
|
var outBuf, errBuf bytes.Buffer
|
|
cmd.SetOut(&outBuf)
|
|
cmd.SetErr(&errBuf)
|
|
cmd.SetArgs(ctb.args)
|
|
err := cmd.Execute()
|
|
output := outBuf.String() + errBuf.String()
|
|
|
|
return output, err
|
|
}
|
|
|
|
// executeServiceCommand handles service command execution with stdout/stderr capture
|
|
func (ctb *CommandTestBuilder) executeServiceCommand(cmd *cobra.Command) (string, error) {
|
|
// Capture os.Stdout since service command uses PrintOutput
|
|
oldStdout := os.Stdout
|
|
stdoutR, stdoutW, err := os.Pipe()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create stdout pipe: %w", err)
|
|
}
|
|
os.Stdout = stdoutW
|
|
|
|
// Also capture os.Stderr since PrintError uses it
|
|
oldStderr := os.Stderr
|
|
stderrR, stderrW, err := os.Pipe()
|
|
if err != nil {
|
|
// Clean up stdout pipe before returning error
|
|
_ = stdoutR.Close()
|
|
_ = stdoutW.Close()
|
|
os.Stdout = oldStdout
|
|
return "", fmt.Errorf("failed to create stderr pipe: %w", err)
|
|
}
|
|
os.Stderr = stderrW
|
|
|
|
var cmdErrBuf bytes.Buffer
|
|
cmd.SetErr(&cmdErrBuf)
|
|
cmd.SetArgs(ctb.args)
|
|
err = cmd.Execute()
|
|
|
|
// Close writers and restore
|
|
if closeErr := stdoutW.Close(); closeErr != nil {
|
|
os.Stdout = oldStdout
|
|
os.Stderr = oldStderr
|
|
return "", fmt.Errorf("failed to close stdout writer: %v", closeErr)
|
|
}
|
|
if closeErr := stderrW.Close(); closeErr != nil {
|
|
os.Stdout = oldStdout
|
|
os.Stderr = oldStderr
|
|
return "", fmt.Errorf("failed to close stderr writer: %v", closeErr)
|
|
}
|
|
os.Stdout = oldStdout
|
|
os.Stderr = oldStderr
|
|
|
|
// Read captured output
|
|
var stdoutBuf bytes.Buffer
|
|
if _, readErr := stdoutBuf.ReadFrom(stdoutR); readErr != nil {
|
|
return "", fmt.Errorf("failed to read stdout: %v", readErr)
|
|
}
|
|
|
|
var stderrBuf bytes.Buffer
|
|
if _, readErr := stderrBuf.ReadFrom(stderrR); readErr != nil {
|
|
return "", fmt.Errorf("failed to read stderr: %v", readErr)
|
|
}
|
|
|
|
output := stdoutBuf.String() + stderrBuf.String() + cmdErrBuf.String()
|
|
return output, err
|
|
}
|
|
|
|
// AssertError validates the error state
|
|
func (result *CommandTestResult) AssertError(expectError bool) *CommandTestResult {
|
|
result.t.Helper()
|
|
if expectError && result.Error == nil {
|
|
result.t.Fatalf(shared.ErrTestExpectedError, result.name)
|
|
}
|
|
if !expectError && result.Error != nil {
|
|
result.t.Fatalf(shared.ErrTestUnexpectedWithOutput, result.name, result.Error, result.Output)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// AssertContains validates that output contains expected text
|
|
func (result *CommandTestResult) AssertContains(expected string) *CommandTestResult {
|
|
result.t.Helper()
|
|
if !strings.Contains(result.Output, expected) {
|
|
result.t.Fatalf(shared.ErrTestExpectedOutput, result.name, expected, result.Output)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// AssertNotContains validates that output does not contain specified text
|
|
func (result *CommandTestResult) AssertNotContains(notExpected string) *CommandTestResult {
|
|
result.t.Helper()
|
|
if strings.Contains(result.Output, notExpected) {
|
|
result.t.Fatalf("%s: expected output to not contain %q, got: %s", result.name, notExpected, result.Output)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// AssertExactOutput validates exact output match
|
|
func (result *CommandTestResult) AssertExactOutput(expected string) *CommandTestResult {
|
|
result.t.Helper()
|
|
if result.Output != expected {
|
|
result.t.Fatalf("%s: expected exact output %q, got %q", result.name, expected, result.Output)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// checkJSONFieldValue validates that a JSON field value matches the expected string.
|
|
func (result *CommandTestResult) checkJSONFieldValue(val interface{}, fieldName, expected string) {
|
|
result.t.Helper()
|
|
if fmt.Sprintf("%v", val) != expected {
|
|
result.t.Fatalf(shared.ErrTestJSONFieldMismatch, result.name, fieldName, expected, val)
|
|
}
|
|
}
|
|
|
|
// failMissingJSONField reports a missing JSON field with context.
|
|
func (result *CommandTestResult) failMissingJSONField(fieldName, context string) {
|
|
result.t.Helper()
|
|
result.t.Fatalf("%s: JSON field %q not found%s: %s", result.name, fieldName, context, result.Output)
|
|
}
|
|
|
|
// AssertJSONField validates a specific field in JSON output
|
|
func (result *CommandTestResult) AssertJSONField(fieldPath, expected string) *CommandTestResult {
|
|
result.t.Helper()
|
|
|
|
var data interface{}
|
|
if err := json.Unmarshal([]byte(result.Output), &data); err != nil {
|
|
result.t.Fatalf("%s: failed to parse JSON output: %v, output: %s", result.name, err, result.Output)
|
|
}
|
|
|
|
// Simple field path parsing (can be enhanced later)
|
|
// For now, support simple paths like "$.field", "[0].field" or direct field names
|
|
fieldName := strings.TrimPrefix(fieldPath, "$.")
|
|
|
|
switch v := data.(type) {
|
|
case map[string]interface{}:
|
|
if val, ok := v[fieldName]; ok {
|
|
result.checkJSONFieldValue(val, fieldName, expected)
|
|
} else {
|
|
result.failMissingJSONField(fieldName, " in output")
|
|
}
|
|
case []interface{}:
|
|
// Handle array case - look in first element
|
|
if len(v) > 0 {
|
|
if firstItem, ok := v[0].(map[string]interface{}); ok {
|
|
if val, ok := firstItem[fieldName]; ok {
|
|
result.checkJSONFieldValue(val, fieldName, expected)
|
|
} else {
|
|
result.failMissingJSONField(fieldName, " in first array element")
|
|
}
|
|
} else {
|
|
result.t.Fatalf("%s: first array element is not an object in output: %s", result.name, result.Output)
|
|
}
|
|
} else {
|
|
result.t.Fatalf("%s: JSON array is empty in output: %s", result.name, result.Output)
|
|
}
|
|
default:
|
|
result.t.Fatalf("%s: expected JSON object or array but got %T in output: %s", result.name, data, result.Output)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// AssertEmpty validates that output is empty
|
|
func (result *CommandTestResult) AssertEmpty() *CommandTestResult {
|
|
result.t.Helper()
|
|
if strings.TrimSpace(result.Output) != "" {
|
|
result.t.Fatalf("%s: expected empty output, got: %s", result.name, result.Output)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// AssertNotEmpty validates that output is not empty
|
|
func (result *CommandTestResult) AssertNotEmpty() *CommandTestResult {
|
|
result.t.Helper()
|
|
if strings.TrimSpace(result.Output) == "" {
|
|
result.t.Fatalf("%s: expected non-empty output", result.name)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// MockClientBuilder provides a fluent interface for building complex mock configurations
|
|
type MockClientBuilder struct {
|
|
client *fail2ban.MockClient
|
|
jails []string
|
|
banRecords []fail2ban.BanRecord
|
|
logLines []string
|
|
responses map[string]string
|
|
errors map[string]error
|
|
}
|
|
|
|
// NewMockClientBuilder creates a new mock client builder
|
|
func NewMockClientBuilder() *MockClientBuilder {
|
|
return &MockClientBuilder{
|
|
client: fail2ban.NewMockClient(),
|
|
responses: make(map[string]string),
|
|
errors: make(map[string]error),
|
|
}
|
|
}
|
|
|
|
// WithJails configures available jails
|
|
func (b *MockClientBuilder) WithJails(jails ...string) *MockClientBuilder {
|
|
b.jails = append(b.jails, jails...)
|
|
return b
|
|
}
|
|
|
|
// WithBannedIP adds a banned IP to specific jail
|
|
func (b *MockClientBuilder) WithBannedIP(ip, jail string) *MockClientBuilder {
|
|
if b.client.BanResults == nil {
|
|
b.client.BanResults = make(map[string]map[string]int)
|
|
}
|
|
if b.client.BanResults[ip] == nil {
|
|
b.client.BanResults[ip] = make(map[string]int)
|
|
}
|
|
b.client.BanResults[ip][jail] = 1 // 1 indicates banned
|
|
return b
|
|
}
|
|
|
|
// WithBanRecord adds a ban record
|
|
func (b *MockClientBuilder) WithBanRecord(jail, ip, remaining string) *MockClientBuilder {
|
|
b.banRecords = append(b.banRecords, fail2ban.BanRecord{
|
|
Jail: jail,
|
|
IP: ip,
|
|
Remaining: remaining,
|
|
})
|
|
return b
|
|
}
|
|
|
|
// WithLogLine adds a log line
|
|
func (b *MockClientBuilder) WithLogLine(logLine string) *MockClientBuilder {
|
|
b.logLines = append(b.logLines, logLine)
|
|
return b
|
|
}
|
|
|
|
// WithStatusResponse sets status response for specific target
|
|
func (b *MockClientBuilder) WithStatusResponse(target, response string) *MockClientBuilder {
|
|
if b.client.StatusJailData == nil {
|
|
b.client.StatusJailData = make(map[string]string)
|
|
}
|
|
if target == shared.AllFilter {
|
|
b.client.StatusAllData = response
|
|
} else {
|
|
b.client.StatusJailData[target] = response
|
|
}
|
|
return b
|
|
}
|
|
|
|
// WithBanError sets an error for banning specific IP in jail
|
|
func (b *MockClientBuilder) WithBanError(jail, ip string, err error) *MockClientBuilder {
|
|
b.client.SetBanError(jail, ip, err)
|
|
return b
|
|
}
|
|
|
|
// WithUnbanError sets an error for unbanning specific IP in jail
|
|
func (b *MockClientBuilder) WithUnbanError(jail, ip string, err error) *MockClientBuilder {
|
|
b.client.SetUnbanError(jail, ip, err)
|
|
return b
|
|
}
|
|
|
|
// WithLogError is not supported by MockClient - logs are returned via LogLines field
|
|
// Use WithLogLine to add log entries or modify LogLines directly
|
|
|
|
// Build creates the configured mock client
|
|
func (b *MockClientBuilder) Build() *fail2ban.MockClient {
|
|
// Apply jails
|
|
if len(b.jails) > 0 {
|
|
setMockJails(b.client, b.jails)
|
|
}
|
|
|
|
// Apply ban records
|
|
if len(b.banRecords) > 0 {
|
|
b.client.BanRecords = b.banRecords
|
|
}
|
|
|
|
// Apply log lines
|
|
if len(b.logLines) > 0 {
|
|
b.client.LogLines = b.logLines
|
|
}
|
|
|
|
return b.client
|
|
}
|
|
|
|
// WithMockBuilder configures the test with a MockClientBuilder for advanced mock setup
|
|
func (ctb *CommandTestBuilder) WithMockBuilder(builder *MockClientBuilder) *CommandTestBuilder {
|
|
ctb.mockClient = builder.Build()
|
|
return ctb
|
|
}
|