mirror of
https://github.com/ivuorinen/f2b.git
synced 2026-03-07 15:58:20 +00:00
* fix: repair Renovate config and convert Makefile to go run (#117) - Remove non-existent `github>renovatebot/presets:golang` preset that broke Renovate PR creation - Replace deprecated `fileMatch` with `managerFilePatterns` in customManagers - Rewrite regex to match new Makefile pattern (renovate comment above version variable assignment) - Fix `matchFileNames` glob pattern (`*.mk` -> `**/*.mk`) - Convert all tool invocations from `go install` + global binary to `go run tool@version` for reproducible builds - Convert npm global tools to `npx --yes` invocations - Remove `dev-deps` and `check-deps` targets (tools auto-download) - Add mdformat pre-commit hook with GFM support and config - Add `fmt-md` Makefile target for manual markdown formatting - Update local golangci-lint pre-commit hook to use `go run` - Apply golangci-lint v2.10.1 auto-fixes (fmt.Fprintf optimization) - Add nolint:gosec annotations for legitimate exec.Command usage - Exclude .serena/ from mdformat and megalinter - Add markdown indent_size=unset in .editorconfig for CommonMark compat * chore(deps): update GitHub Actions to latest versions - anthropics/claude-code-action: v1.0.34 -> v1.0.64 - actions/setup-go: v6.2.0 -> v6.3.0 - actions/upload-artifact: v6.0.0 -> v7.0.0 - goreleaser/goreleaser-action: v6.4.0 -> v7.0.0 - docker/login-action: v3.6.0 -> v3.7.0 - ivuorinen/actions: v2026.01.21 -> v2026.02.24 * fix: address code review feedback - Fix issue template YAML frontmatter (replace underscore separators with proper --- delimiters); exclude templates from mdformat - Replace string(rune(n)) with strconv.Itoa(n) in test files to produce deterministic numeric directory names instead of Unicode characters - Remove stale `make dev-deps` reference in README, replace with `make dev-setup` - Extract ban/unban format strings into shared.MetricsFmtBanOperations and shared.MetricsFmtUnbanOperations constants - Replace hardcoded coverage percentages in README with evergreen phrasing * fix: address round 2 code review feedback for PR #120 - Fix corrupted path traversal example in docs/security.md - Fix Renovate .mk regex to match nested paths (.*\.mk$) - Update checkmake pre-commit hook to v0.3.2 to match Makefile - Add sync.WaitGroup to unsynchronized goroutines in security tests - Fix fmt-md target to use pre-commit run mdformat - Pin markdownlint-cli2 to v0.21.0 in lint-md target - Standardize //nolint:gosec to // #nosec annotations for gosec CLI * fix(ci): install PyYAML dependency for PR lint workflow The pr-lint workflow uses ivuorinen/actions/pr-lint which internally calls validate-inputs running a Python script that imports yaml. Python was set up but PyYAML was never installed, causing ModuleNotFoundError at runtime. * fix: address round 3 code review feedback for PR #120 - Wrap Windows-style path traversal example in backtick code span so backslashes render literally in docs/security.md - Add Renovate-managed MARKDOWNLINT_CLI2_VERSION variable in Makefile to match the pattern used by all other tool versions
785 lines
19 KiB
Go
785 lines
19 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/ivuorinen/f2b/fail2ban"
|
|
)
|
|
|
|
func TestParseLogLevel(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
level string
|
|
expected logrus.Level
|
|
}{
|
|
{
|
|
name: "debug level",
|
|
level: "debug",
|
|
expected: logrus.DebugLevel,
|
|
},
|
|
{
|
|
name: "info level",
|
|
level: "info",
|
|
expected: logrus.InfoLevel,
|
|
},
|
|
{
|
|
name: "warn level",
|
|
level: "warn",
|
|
expected: logrus.WarnLevel,
|
|
},
|
|
{
|
|
name: "warning level",
|
|
level: "warning",
|
|
expected: logrus.WarnLevel,
|
|
},
|
|
{
|
|
name: "error level",
|
|
level: "error",
|
|
expected: logrus.ErrorLevel,
|
|
},
|
|
{
|
|
name: "fatal level",
|
|
level: "fatal",
|
|
expected: logrus.FatalLevel,
|
|
},
|
|
{
|
|
name: "panic level",
|
|
level: "panic",
|
|
expected: logrus.PanicLevel,
|
|
},
|
|
{
|
|
name: "unknown level defaults to info",
|
|
level: "unknown",
|
|
expected: logrus.InfoLevel,
|
|
},
|
|
{
|
|
name: "empty level defaults to info",
|
|
level: "",
|
|
expected: logrus.InfoLevel,
|
|
},
|
|
{
|
|
name: "uppercase level",
|
|
level: "DEBUG",
|
|
expected: logrus.InfoLevel, // case sensitive, so falls back to default
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := parseLogLevel(tt.level)
|
|
if result != tt.expected {
|
|
t.Errorf("parseLogLevel(%q) = %v, want %v", tt.level, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfigDefaults(t *testing.T) {
|
|
// Test that Config struct has reasonable defaults
|
|
config := Config{}
|
|
|
|
// Initially empty
|
|
if config.LogDir != "" {
|
|
t.Errorf("expected empty LogDir, got %q", config.LogDir)
|
|
}
|
|
if config.FilterDir != "" {
|
|
t.Errorf("expected empty FilterDir, got %q", config.FilterDir)
|
|
}
|
|
if config.Format != "" {
|
|
t.Errorf("expected empty Format, got %q", config.Format)
|
|
}
|
|
}
|
|
|
|
func TestEnvironmentVariableSetup(t *testing.T) {
|
|
// Save original environment
|
|
// Set up environment variables using t.Setenv for automatic cleanup
|
|
t.Setenv("F2B_LOG_DIR", os.Getenv("F2B_LOG_DIR"))
|
|
t.Setenv("F2B_FILTER_DIR", os.Getenv("F2B_FILTER_DIR"))
|
|
t.Setenv("F2B_LOG_LEVEL", os.Getenv("F2B_LOG_LEVEL"))
|
|
t.Setenv("F2B_LOG_FILE", os.Getenv("F2B_LOG_FILE"))
|
|
|
|
tests := []struct {
|
|
name string
|
|
envVar string
|
|
envValue string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "F2B_LOG_DIR environment variable",
|
|
envVar: "F2B_LOG_DIR",
|
|
envValue: "/custom/log/dir",
|
|
expected: "/custom/log/dir",
|
|
},
|
|
{
|
|
name: "F2B_FILTER_DIR environment variable",
|
|
envVar: "F2B_FILTER_DIR",
|
|
envValue: "/custom/filter/dir",
|
|
expected: "/custom/filter/dir",
|
|
},
|
|
{
|
|
name: "F2B_LOG_LEVEL environment variable",
|
|
envVar: "F2B_LOG_LEVEL",
|
|
envValue: "debug",
|
|
expected: "debug",
|
|
},
|
|
{
|
|
name: "F2B_LOG_FILE environment variable",
|
|
envVar: "F2B_LOG_FILE",
|
|
envValue: "/tmp/f2b.log",
|
|
expected: "/tmp/f2b.log",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Set environment variable using t.Setenv for automatic cleanup
|
|
t.Setenv(tt.envVar, tt.envValue)
|
|
|
|
// Get the value
|
|
result := os.Getenv(tt.envVar)
|
|
if result != tt.expected {
|
|
t.Errorf("expected %q, got %q", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfigStructure(t *testing.T) {
|
|
config := Config{
|
|
LogDir: "/test/log",
|
|
FilterDir: "/test/filter",
|
|
Format: "json",
|
|
}
|
|
|
|
if config.LogDir != "/test/log" {
|
|
t.Errorf("expected LogDir '/test/log', got %q", config.LogDir)
|
|
}
|
|
if config.FilterDir != "/test/filter" {
|
|
t.Errorf("expected FilterDir '/test/filter', got %q", config.FilterDir)
|
|
}
|
|
if config.Format != "json" {
|
|
t.Errorf("expected Format 'json', got %q", config.Format)
|
|
}
|
|
}
|
|
|
|
func TestCompletionCmdStructure(t *testing.T) {
|
|
cmd := completionCmd()
|
|
|
|
if cmd.Use != "completion [bash|zsh|fish|powershell]" {
|
|
t.Errorf("unexpected completion command Use: %q", cmd.Use)
|
|
}
|
|
|
|
if cmd.Short != "Generate shell completion scripts" {
|
|
t.Errorf("unexpected completion command Short: %q", cmd.Short)
|
|
}
|
|
|
|
expectedValidArgs := []string{"bash", "zsh", "fish", "powershell"}
|
|
if len(cmd.ValidArgs) != len(expectedValidArgs) {
|
|
t.Errorf("expected %d ValidArgs, got %d", len(expectedValidArgs), len(cmd.ValidArgs))
|
|
}
|
|
|
|
for i, expected := range expectedValidArgs {
|
|
if i >= len(cmd.ValidArgs) || cmd.ValidArgs[i] != expected {
|
|
t.Errorf("expected ValidArgs[%d] = %q, got %q", i, expected, cmd.ValidArgs[i])
|
|
}
|
|
}
|
|
|
|
if !cmd.DisableFlagsInUseLine {
|
|
t.Errorf("expected DisableFlagsInUseLine to be true")
|
|
}
|
|
}
|
|
|
|
func TestGlobalVariables(t *testing.T) {
|
|
// Test that global variables are properly initialized
|
|
if rootCmd == nil {
|
|
t.Fatal("rootCmd should be initialized")
|
|
}
|
|
|
|
if rootCmd.Use != "f2b" {
|
|
t.Errorf("expected rootCmd.Use to be 'f2b', got %q", rootCmd.Use)
|
|
}
|
|
|
|
if rootCmd.Short != "Fail2Ban CLI helper" {
|
|
t.Errorf("expected rootCmd.Short to be 'Fail2Ban CLI helper', got %q", rootCmd.Short)
|
|
}
|
|
|
|
expectedLong := "Fail2Ban CLI tool implemented in Go using Cobra."
|
|
if rootCmd.Long != expectedLong {
|
|
t.Errorf("expected rootCmd.Long to be %q, got %q", expectedLong, rootCmd.Long)
|
|
}
|
|
}
|
|
|
|
// BenchmarkParseLogLevel benchmarks the log level parsing function
|
|
func BenchmarkParseLogLevel(b *testing.B) {
|
|
levels := []string{"debug", "info", "warn", "error", "unknown"}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
level := levels[i%len(levels)]
|
|
parseLogLevel(level)
|
|
}
|
|
}
|
|
|
|
// TestDefaultValues tests the default values used in the configuration
|
|
func TestDefaultValues(t *testing.T) {
|
|
// Clear environment variables for this test using t.Setenv
|
|
t.Setenv("F2B_LOG_DIR", "")
|
|
t.Setenv("F2B_FILTER_DIR", "")
|
|
|
|
// Test default values when environment variables are not set
|
|
logDir := os.Getenv("F2B_LOG_DIR")
|
|
if logDir != "" {
|
|
t.Errorf("expected empty F2B_LOG_DIR, got %q", logDir)
|
|
}
|
|
|
|
filterDir := os.Getenv("F2B_FILTER_DIR")
|
|
if filterDir != "" {
|
|
t.Errorf("expected empty F2B_FILTER_DIR, got %q", filterDir)
|
|
}
|
|
}
|
|
|
|
func TestExecute(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupClient func() fail2ban.Client
|
|
config Config
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "successful execution with mock client",
|
|
setupClient: func() fail2ban.Client {
|
|
return fail2ban.NewMockClient()
|
|
},
|
|
config: Config{
|
|
LogDir: "/tmp/test",
|
|
FilterDir: "/tmp/filters",
|
|
Format: "plain",
|
|
},
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "execution with json format",
|
|
setupClient: func() fail2ban.Client {
|
|
return fail2ban.NewMockClient()
|
|
},
|
|
config: Config{
|
|
LogDir: "/var/log",
|
|
FilterDir: "/etc/fail2ban/filter.d",
|
|
Format: "json",
|
|
},
|
|
wantError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client := tt.setupClient()
|
|
|
|
// Capture stdout to prevent output during tests
|
|
oldStdout := os.Stdout
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatalf("failed to create pipe: %v", err)
|
|
}
|
|
os.Stdout = w
|
|
|
|
// Set up a simple test command that will exit quickly
|
|
originalArgs := os.Args
|
|
os.Args = []string{"f2b", "version"}
|
|
|
|
err = Execute(client, tt.config)
|
|
|
|
// Restore stdout
|
|
if err := w.Close(); err != nil {
|
|
t.Fatalf("failed to close writer: %v", err)
|
|
}
|
|
os.Stdout = oldStdout
|
|
os.Args = originalArgs
|
|
|
|
// Read and discard output
|
|
var buf bytes.Buffer
|
|
if _, err := buf.ReadFrom(r); err != nil {
|
|
t.Fatalf("failed to read output: %v", err)
|
|
}
|
|
|
|
AssertError(t, err, tt.wantError, tt.name)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecuteWithRealCommands(t *testing.T) {
|
|
// Test that Execute properly adds all commands
|
|
client := fail2ban.NewMockClient()
|
|
config := Config{
|
|
LogDir: "/tmp",
|
|
FilterDir: "/tmp",
|
|
Format: "plain",
|
|
}
|
|
|
|
// Create a new root command to test command registration
|
|
originalRootCmd := rootCmd
|
|
rootCmd = &cobra.Command{
|
|
Use: "f2b",
|
|
Short: "Fail2Ban CLI helper",
|
|
Long: "Fail2Ban CLI tool implemented in Go using Cobra.",
|
|
}
|
|
|
|
// Capture stdout
|
|
oldStdout := os.Stdout
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatalf("failed to create pipe: %v", err)
|
|
}
|
|
os.Stdout = w
|
|
|
|
originalArgs := os.Args
|
|
os.Args = []string{"f2b", "help"}
|
|
|
|
err = Execute(client, config)
|
|
|
|
// Restore
|
|
if err := w.Close(); err != nil {
|
|
t.Fatalf("failed to close writer: %v", err)
|
|
}
|
|
os.Stdout = oldStdout
|
|
os.Args = originalArgs
|
|
rootCmd = originalRootCmd
|
|
|
|
// Read output
|
|
var buf bytes.Buffer
|
|
if _, err := buf.ReadFrom(r); err != nil {
|
|
t.Fatalf("failed to read output: %v", err)
|
|
}
|
|
output := buf.String()
|
|
|
|
AssertError(t, err, false, "root help command")
|
|
|
|
// Check that help output contains expected commands
|
|
expectedCommands := []string{
|
|
"list-jails",
|
|
"status",
|
|
"banned",
|
|
"ban",
|
|
"unban",
|
|
"test",
|
|
"logs",
|
|
"logs-watch",
|
|
"service",
|
|
"version",
|
|
"test-filter",
|
|
"completion",
|
|
}
|
|
for _, cmd := range expectedCommands {
|
|
if !strings.Contains(output, cmd) {
|
|
t.Errorf("expected help output to contain command %q", cmd)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCompletionCmdExecution(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
wantOutput string
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "bash completion",
|
|
args: []string{"bash"},
|
|
wantOutput: "__start_f2b",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "zsh completion",
|
|
args: []string{"zsh"},
|
|
wantOutput: "#compdef f2b",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "fish completion",
|
|
args: []string{"fish"},
|
|
wantOutput: "complete -c f2b",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "powershell completion",
|
|
args: []string{"powershell"},
|
|
wantOutput: "Register-ArgumentCompleter",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "unsupported shell",
|
|
args: []string{"unsupported"},
|
|
wantError: true, // Cobra returns an error for invalid args due to OnlyValidArgs
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Framework doesn't support completion cmd yet, so keeping manual approach:
|
|
// Create a proper root command structure for the test
|
|
testRoot := &cobra.Command{
|
|
Use: "f2b",
|
|
Short: "Test root command",
|
|
}
|
|
|
|
// Add mock client for commands that need it
|
|
mockClient := NewMockClient()
|
|
testConfig := Config{Format: "plain"}
|
|
|
|
// Add all the f2b subcommands to create a realistic structure
|
|
testRoot.AddCommand(ListJailsCmd(mockClient, &testConfig))
|
|
testRoot.AddCommand(StatusCmd(mockClient, &testConfig))
|
|
testRoot.AddCommand(BannedCmd(mockClient, &testConfig))
|
|
testRoot.AddCommand(BanCmd(mockClient, &testConfig))
|
|
testRoot.AddCommand(UnbanCmd(mockClient, &testConfig))
|
|
testRoot.AddCommand(TestIPCmd(mockClient, &testConfig))
|
|
testRoot.AddCommand(LogsCmd(mockClient, &testConfig))
|
|
testRoot.AddCommand(LogsWatchCmd(context.Background(), mockClient, &testConfig))
|
|
testRoot.AddCommand(ServiceCmd(&testConfig))
|
|
testRoot.AddCommand(VersionCmd(&testConfig))
|
|
testRoot.AddCommand(TestFilterCmd(mockClient, &testConfig))
|
|
testRoot.AddCommand(completionCmd())
|
|
|
|
// Execute the completion command via the root
|
|
// Capture stdout
|
|
var outBuf bytes.Buffer
|
|
testRoot.SetOut(&outBuf)
|
|
|
|
// Capture stderr
|
|
var errBuf bytes.Buffer
|
|
testRoot.SetErr(&errBuf)
|
|
|
|
args := append([]string{"completion"}, tt.args...)
|
|
testRoot.SetArgs(args)
|
|
err := testRoot.Execute()
|
|
|
|
AssertError(t, err, tt.wantError, tt.name)
|
|
|
|
output := outBuf.String() + errBuf.String()
|
|
if tt.wantOutput != "" && !tt.wantError {
|
|
// Check for substring anywhere in the output, ignoring leading/trailing whitespace
|
|
if !strings.Contains(output, tt.wantOutput) {
|
|
t.Errorf("expected output to contain %q, got %q", tt.wantOutput, strings.TrimSpace(output))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInitFunctionCoverage(t *testing.T) {
|
|
// Test that init function sets up flags correctly
|
|
// We can't directly test init() but we can test its effects
|
|
|
|
// Test that persistent flags are set
|
|
if rootCmd.PersistentFlags().Lookup("log-dir") == nil {
|
|
t.Errorf("expected log-dir persistent flag to be set")
|
|
}
|
|
|
|
if rootCmd.PersistentFlags().Lookup("filter-dir") == nil {
|
|
t.Errorf("expected filter-dir persistent flag to be set")
|
|
}
|
|
|
|
if rootCmd.PersistentFlags().Lookup("format") == nil {
|
|
t.Errorf("expected format persistent flag to be set")
|
|
}
|
|
|
|
if rootCmd.PersistentFlags().Lookup("log-file") == nil {
|
|
t.Errorf("expected log-file persistent flag to be set")
|
|
}
|
|
|
|
if rootCmd.PersistentFlags().Lookup("log-level") == nil {
|
|
t.Errorf("expected log-level persistent flag to be set")
|
|
}
|
|
}
|
|
|
|
func TestPersistentPreRun(t *testing.T) {
|
|
// Test the PersistentPreRun function
|
|
if rootCmd.PersistentPreRun == nil {
|
|
t.Errorf("expected PersistentPreRun to be set")
|
|
return
|
|
}
|
|
|
|
// Create a temporary log file
|
|
tmpFile, err := os.CreateTemp(t.TempDir(), "f2b-test-*.log")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp file: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := os.Remove(tmpFile.Name()); err != nil { // #nosec G703 -- test file, path from CreateTemp
|
|
t.Fatalf("failed to remove temp file: %v", err)
|
|
}
|
|
}()
|
|
defer func() {
|
|
if err := tmpFile.Close(); err != nil {
|
|
t.Fatalf("failed to close temp file: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Test with log file flag
|
|
cmd := &cobra.Command{}
|
|
cmd.Flags().String("log-file", tmpFile.Name(), "test log file")
|
|
cmd.Flags().String("log-level", "debug", "test log level")
|
|
|
|
// Save original logger output
|
|
originalOutput := Logger.Out
|
|
|
|
// Run PersistentPreRun
|
|
rootCmd.PersistentPreRun(cmd, []string{})
|
|
|
|
// Restore original logger output
|
|
Logger.SetOutput(originalOutput)
|
|
|
|
// Test log level parsing
|
|
tests := []struct {
|
|
name string
|
|
logLevel string
|
|
}{
|
|
{"debug", "debug"},
|
|
{"info", "info"},
|
|
{"warn", "warn"},
|
|
{"error", "error"},
|
|
{"invalid", "invalid"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run("log_level_"+tt.name, func(_ *testing.T) {
|
|
cmd := &cobra.Command{}
|
|
cmd.Flags().String("log-file", "", "")
|
|
cmd.Flags().String("log-level", tt.logLevel, "")
|
|
|
|
// This should not panic
|
|
rootCmd.PersistentPreRun(cmd, []string{})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPersistentPreRunWithInvalidLogFile(t *testing.T) {
|
|
// Test PersistentPreRun with invalid log file path
|
|
cmd := &cobra.Command{}
|
|
cmd.Flags().String("log-file", "/invalid/path/to/logfile.log", "invalid log file")
|
|
cmd.Flags().String("log-level", "info", "test log level")
|
|
|
|
// Capture stderr to check for error message
|
|
oldStderr := os.Stderr
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatalf("failed to create pipe: %v", err)
|
|
}
|
|
os.Stderr = w
|
|
|
|
// This should handle the error gracefully
|
|
rootCmd.PersistentPreRun(cmd, []string{})
|
|
|
|
if err := w.Close(); err != nil {
|
|
t.Fatalf("failed to close writer: %v", err)
|
|
}
|
|
os.Stderr = oldStderr
|
|
|
|
var buf bytes.Buffer
|
|
if _, err := buf.ReadFrom(r); err != nil {
|
|
t.Fatalf("failed to read output: %v", err)
|
|
}
|
|
output := buf.String()
|
|
|
|
// Should contain error message about failed to open log file
|
|
if !strings.Contains(output, "Failed to open log file") {
|
|
t.Errorf("expected error message about failed to open log file, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestCompletionCmdLongDescription(t *testing.T) {
|
|
cmd := completionCmd()
|
|
|
|
// Test that the long description contains instructions for all shells
|
|
expectedShells := []string{"Bash:", "Zsh:", "Fish:", "PowerShell:"}
|
|
for _, shell := range expectedShells {
|
|
if !strings.Contains(cmd.Long, shell) {
|
|
t.Errorf("expected completion long description to contain %q", shell)
|
|
}
|
|
}
|
|
|
|
// Test that it contains example commands
|
|
expectedExamples := []string{
|
|
"f2b completion bash",
|
|
"f2b completion zsh",
|
|
"f2b completion fish",
|
|
"f2b completion powershell",
|
|
}
|
|
for _, example := range expectedExamples {
|
|
if !strings.Contains(cmd.Long, example) {
|
|
t.Errorf("expected completion long description to contain example %q", example)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGlobalConfigVariable(t *testing.T) {
|
|
// Test that global cfg variable can be accessed and modified
|
|
originalCfg := cfg
|
|
defer func() { cfg = originalCfg }()
|
|
|
|
cfg = Config{
|
|
LogDir: "/test/log",
|
|
FilterDir: "/test/filter",
|
|
Format: "json",
|
|
}
|
|
|
|
if cfg.LogDir != "/test/log" {
|
|
t.Errorf("expected LogDir to be '/test/log', got %q", cfg.LogDir)
|
|
}
|
|
if cfg.FilterDir != "/test/filter" {
|
|
t.Errorf("expected FilterDir to be '/test/filter', got %q", cfg.FilterDir)
|
|
}
|
|
if cfg.Format != "json" {
|
|
t.Errorf("expected Format to be 'json', got %q", cfg.Format)
|
|
}
|
|
}
|
|
|
|
// TestExecuteIntegration tests the Execute function with different command combinations
|
|
func TestExecuteIntegration(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
config Config
|
|
setupEnv func()
|
|
cleanup func()
|
|
}{
|
|
{
|
|
name: "execute with environment variables",
|
|
args: []string{"f2b", "version"},
|
|
config: Config{
|
|
LogDir: "/tmp/test",
|
|
FilterDir: "/tmp/filters",
|
|
Format: "plain",
|
|
},
|
|
setupEnv: func() {
|
|
// Environment variables will be set using t.Setenv in test loop
|
|
},
|
|
cleanup: func() {
|
|
// Cleanup handled automatically by t.Setenv
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Integration test requires manual approach:
|
|
// Set up environment variables using t.Setenv for automatic cleanup
|
|
if tt.config.LogDir != "" {
|
|
t.Setenv("F2B_LOG_DIR", tt.config.LogDir)
|
|
}
|
|
if tt.config.FilterDir != "" {
|
|
t.Setenv("F2B_FILTER_DIR", tt.config.FilterDir)
|
|
}
|
|
|
|
client := fail2ban.NewMockClient()
|
|
|
|
// Capture output
|
|
oldStdout := os.Stdout
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatalf("failed to create pipe: %v", err)
|
|
}
|
|
os.Stdout = w
|
|
|
|
originalArgs := os.Args
|
|
os.Args = tt.args
|
|
|
|
err = Execute(client, tt.config)
|
|
|
|
// Restore
|
|
if closeErr := w.Close(); closeErr != nil {
|
|
t.Fatalf("failed to close writer: %v", closeErr)
|
|
}
|
|
os.Stdout = oldStdout
|
|
os.Args = originalArgs
|
|
|
|
// Read output
|
|
var buf bytes.Buffer
|
|
if _, readErr := buf.ReadFrom(r); readErr != nil {
|
|
t.Fatalf("failed to read output: %v", readErr)
|
|
}
|
|
|
|
AssertError(t, err, false, tt.name)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompletionCmdWithUnsupportedShell(t *testing.T) {
|
|
cmd := completionCmd()
|
|
|
|
// Capture stderr to check for error message
|
|
var errBuf bytes.Buffer
|
|
cmd.SetErr(&errBuf)
|
|
|
|
cmd.SetArgs([]string{"invalid-shell"})
|
|
err := cmd.Execute()
|
|
|
|
// Should return error due to Cobra's OnlyValidArgs validation
|
|
if err == nil {
|
|
t.Errorf("expected error for invalid shell type")
|
|
}
|
|
|
|
// Error should mention invalid argument
|
|
if !strings.Contains(err.Error(), "invalid argument") && !strings.Contains(err.Error(), "invalid") {
|
|
t.Errorf("expected error message about invalid argument, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// Benchmark tests
|
|
func BenchmarkParseLogLevelExtended(b *testing.B) {
|
|
levels := []string{"debug", "info", "warn", "warning", "error", "fatal", "panic", "invalid", ""}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
level := levels[i%len(levels)]
|
|
parseLogLevel(level)
|
|
}
|
|
}
|
|
|
|
func BenchmarkExecute(b *testing.B) {
|
|
client := fail2ban.NewMockClient()
|
|
config := Config{
|
|
LogDir: "/tmp",
|
|
FilterDir: "/tmp",
|
|
Format: "plain",
|
|
}
|
|
|
|
// Suppress output
|
|
oldStdout := os.Stdout
|
|
devNull, err := os.Open(os.DevNull)
|
|
if err != nil {
|
|
b.Fatalf("failed to open dev null: %v", err)
|
|
}
|
|
defer func() {
|
|
if cerr := devNull.Close(); cerr != nil {
|
|
b.Fatalf("failed to close dev null: %v", cerr)
|
|
}
|
|
}()
|
|
os.Stdout = devNull
|
|
|
|
defer func() {
|
|
os.Stdout = oldStdout
|
|
}()
|
|
|
|
originalArgs := os.Args
|
|
defer func() {
|
|
os.Args = originalArgs
|
|
}()
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
os.Args = []string{"f2b", "version"}
|
|
if err := Execute(client, config); err != nil {
|
|
b.Fatalf("execute failed: %v", err)
|
|
}
|
|
}
|
|
}
|