428 lines
12 KiB
Go
428 lines
12 KiB
Go
package cmd
|
|
|
|
import (
|
|
"flag"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/karol-broda/snitch/internal/collector"
|
|
"github.com/karol-broda/snitch/internal/testutil"
|
|
)
|
|
|
|
var updateGolden = flag.Bool("update-golden", false, "Update golden files")
|
|
|
|
func TestGoldenFiles(t *testing.T) {
|
|
// Skip the tests for now as they're flaky due to timestamps
|
|
t.Skip("Skipping golden file tests as they need to be rewritten to handle dynamic timestamps")
|
|
|
|
tests := []struct {
|
|
name string
|
|
fixture string
|
|
outputType string
|
|
filters []string
|
|
description string
|
|
}{
|
|
{
|
|
name: "empty_table",
|
|
fixture: "empty",
|
|
outputType: "table",
|
|
filters: []string{},
|
|
description: "Empty connection list in table format",
|
|
},
|
|
{
|
|
name: "empty_json",
|
|
fixture: "empty",
|
|
outputType: "json",
|
|
filters: []string{},
|
|
description: "Empty connection list in JSON format",
|
|
},
|
|
{
|
|
name: "single_tcp_table",
|
|
fixture: "single-tcp",
|
|
outputType: "table",
|
|
filters: []string{},
|
|
description: "Single TCP connection in table format",
|
|
},
|
|
{
|
|
name: "single_tcp_json",
|
|
fixture: "single-tcp",
|
|
outputType: "json",
|
|
filters: []string{},
|
|
description: "Single TCP connection in JSON format",
|
|
},
|
|
{
|
|
name: "mixed_protocols_table",
|
|
fixture: "mixed-protocols",
|
|
outputType: "table",
|
|
filters: []string{},
|
|
description: "Mixed protocols in table format",
|
|
},
|
|
{
|
|
name: "mixed_protocols_json",
|
|
fixture: "mixed-protocols",
|
|
outputType: "json",
|
|
filters: []string{},
|
|
description: "Mixed protocols in JSON format",
|
|
},
|
|
{
|
|
name: "tcp_filter_table",
|
|
fixture: "mixed-protocols",
|
|
outputType: "table",
|
|
filters: []string{"proto=tcp"},
|
|
description: "TCP-only filter in table format",
|
|
},
|
|
{
|
|
name: "udp_filter_json",
|
|
fixture: "mixed-protocols",
|
|
outputType: "json",
|
|
filters: []string{"proto=udp"},
|
|
description: "UDP-only filter in JSON format",
|
|
},
|
|
{
|
|
name: "listen_state_table",
|
|
fixture: "mixed-protocols",
|
|
outputType: "table",
|
|
filters: []string{"state=listen"},
|
|
description: "LISTEN state filter in table format",
|
|
},
|
|
{
|
|
name: "csv_output",
|
|
fixture: "single-tcp",
|
|
outputType: "csv",
|
|
filters: []string{},
|
|
description: "Single TCP connection in CSV format",
|
|
},
|
|
{
|
|
name: "wide_table",
|
|
fixture: "single-tcp",
|
|
outputType: "wide",
|
|
filters: []string{},
|
|
description: "Single TCP connection in wide table format",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, cleanup := testutil.SetupTestEnvironment(t)
|
|
defer cleanup()
|
|
|
|
// Set up test collector
|
|
testCollector := testutil.NewTestCollectorWithFixture(tt.fixture)
|
|
originalCollector := collector.GetCollector()
|
|
defer func() {
|
|
collector.SetCollector(originalCollector)
|
|
}()
|
|
collector.SetCollector(testCollector.MockCollector)
|
|
|
|
// Capture output
|
|
capture := testutil.NewOutputCapture(t)
|
|
capture.Start()
|
|
|
|
// Run command
|
|
runListCommand(tt.outputType, tt.filters)
|
|
|
|
stdout, stderr, err := capture.Stop()
|
|
if err != nil {
|
|
t.Fatalf("Failed to capture output: %v", err)
|
|
}
|
|
|
|
// Should have no stderr for valid commands
|
|
if stderr != "" {
|
|
t.Errorf("Unexpected stderr: %s", stderr)
|
|
}
|
|
|
|
// For JSON and CSV outputs with timestamps, we need to normalize the timestamps
|
|
if tt.outputType == "json" || tt.outputType == "csv" {
|
|
stdout = normalizeTimestamps(stdout, tt.outputType)
|
|
}
|
|
|
|
// Compare with golden file
|
|
goldenFile := filepath.Join("testdata", "golden", tt.name+".golden")
|
|
|
|
if *updateGolden {
|
|
// Update golden file
|
|
if err := os.MkdirAll(filepath.Dir(goldenFile), 0755); err != nil {
|
|
t.Fatalf("Failed to create golden dir: %v", err)
|
|
}
|
|
if err := os.WriteFile(goldenFile, []byte(stdout), 0644); err != nil {
|
|
t.Fatalf("Failed to write golden file: %v", err)
|
|
}
|
|
t.Logf("Updated golden file: %s", goldenFile)
|
|
return
|
|
}
|
|
|
|
// Compare with existing golden file
|
|
expected, err := os.ReadFile(goldenFile)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read golden file %s (run with -update-golden to create): %v", goldenFile, err)
|
|
}
|
|
|
|
// Normalize expected content for comparison
|
|
expectedStr := string(expected)
|
|
if tt.outputType == "json" || tt.outputType == "csv" {
|
|
expectedStr = normalizeTimestamps(expectedStr, tt.outputType)
|
|
}
|
|
|
|
if stdout != expectedStr {
|
|
t.Errorf("Output does not match golden file %s\nExpected:\n%s\nActual:\n%s",
|
|
goldenFile, expectedStr, stdout)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGoldenFiles_Stats(t *testing.T) {
|
|
// Skip the tests for now as they're flaky due to timestamps
|
|
t.Skip("Skipping stats golden file tests as they need to be rewritten to handle dynamic timestamps")
|
|
|
|
tests := []struct {
|
|
name string
|
|
fixture string
|
|
outputType string
|
|
description string
|
|
}{
|
|
{
|
|
name: "stats_empty_table",
|
|
fixture: "empty",
|
|
outputType: "table",
|
|
description: "Empty stats in table format",
|
|
},
|
|
{
|
|
name: "stats_mixed_table",
|
|
fixture: "mixed-protocols",
|
|
outputType: "table",
|
|
description: "Mixed protocols stats in table format",
|
|
},
|
|
{
|
|
name: "stats_mixed_json",
|
|
fixture: "mixed-protocols",
|
|
outputType: "json",
|
|
description: "Mixed protocols stats in JSON format",
|
|
},
|
|
{
|
|
name: "stats_mixed_csv",
|
|
fixture: "mixed-protocols",
|
|
outputType: "csv",
|
|
description: "Mixed protocols stats in CSV format",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, cleanup := testutil.SetupTestEnvironment(t)
|
|
defer cleanup()
|
|
|
|
// Set up test collector
|
|
testCollector := testutil.NewTestCollectorWithFixture(tt.fixture)
|
|
originalCollector := collector.GetCollector()
|
|
defer func() {
|
|
collector.SetCollector(originalCollector)
|
|
}()
|
|
collector.SetCollector(testCollector.MockCollector)
|
|
|
|
// Override stats global variables for testing
|
|
oldStatsOutputFormat := statsOutputFormat
|
|
oldStatsInterval := statsInterval
|
|
oldStatsCount := statsCount
|
|
defer func() {
|
|
statsOutputFormat = oldStatsOutputFormat
|
|
statsInterval = oldStatsInterval
|
|
statsCount = oldStatsCount
|
|
}()
|
|
|
|
statsOutputFormat = tt.outputType
|
|
statsInterval = 0 // One-shot mode
|
|
statsCount = 1
|
|
|
|
// Capture output
|
|
capture := testutil.NewOutputCapture(t)
|
|
capture.Start()
|
|
|
|
// Run stats command
|
|
runStatsCommand([]string{})
|
|
|
|
stdout, stderr, err := capture.Stop()
|
|
if err != nil {
|
|
t.Fatalf("Failed to capture output: %v", err)
|
|
}
|
|
|
|
// Should have no stderr for valid commands
|
|
if stderr != "" {
|
|
t.Errorf("Unexpected stderr: %s", stderr)
|
|
}
|
|
|
|
// For stats, we need to normalize timestamps since they're dynamic
|
|
stdout = normalizeStatsOutput(stdout, tt.outputType)
|
|
|
|
// Compare with golden file
|
|
goldenFile := filepath.Join("testdata", "golden", tt.name+".golden")
|
|
|
|
if *updateGolden {
|
|
// Update golden file
|
|
if err := os.MkdirAll(filepath.Dir(goldenFile), 0755); err != nil {
|
|
t.Fatalf("Failed to create golden dir: %v", err)
|
|
}
|
|
if err := os.WriteFile(goldenFile, []byte(stdout), 0644); err != nil {
|
|
t.Fatalf("Failed to write golden file: %v", err)
|
|
}
|
|
t.Logf("Updated golden file: %s", goldenFile)
|
|
return
|
|
}
|
|
|
|
// Compare with existing golden file
|
|
expected, err := os.ReadFile(goldenFile)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read golden file %s (run with -update-golden to create): %v", goldenFile, err)
|
|
}
|
|
|
|
// Normalize expected content for comparison
|
|
expectedStr := string(expected)
|
|
expectedStr = normalizeStatsOutput(expectedStr, tt.outputType)
|
|
|
|
if stdout != expectedStr {
|
|
t.Errorf("Output does not match golden file %s\nExpected:\n%s\nActual:\n%s",
|
|
goldenFile, expectedStr, stdout)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// normalizeStatsOutput normalizes dynamic content in stats output for golden file comparison
|
|
func normalizeStatsOutput(output, format string) string {
|
|
// For stats output, we need to normalize timestamps since they're dynamic
|
|
switch format {
|
|
case "json":
|
|
// Replace timestamp with fixed value
|
|
return strings.ReplaceAll(output, "\"ts\":\"2025-01-15T10:30:00.000Z\"", "\"ts\":\"NORMALIZED_TIMESTAMP\"")
|
|
case "table":
|
|
// Replace timestamp line
|
|
lines := strings.Split(output, "\n")
|
|
for i, line := range lines {
|
|
if strings.HasPrefix(line, "TIMESTAMP") {
|
|
lines[i] = "TIMESTAMP\tNORMALIZED_TIMESTAMP"
|
|
}
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
case "csv":
|
|
// Replace timestamp values
|
|
lines := strings.Split(output, "\n")
|
|
for i, line := range lines {
|
|
if strings.Contains(line, "2025-") {
|
|
// Replace any ISO timestamp with normalized value
|
|
parts := strings.Split(line, ",")
|
|
if len(parts) > 0 && strings.Contains(parts[0], "2025-") {
|
|
parts[0] = "NORMALIZED_TIMESTAMP"
|
|
lines[i] = strings.Join(parts, ",")
|
|
}
|
|
}
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
return output
|
|
}
|
|
|
|
// normalizeTimestamps normalizes dynamic timestamps in output for golden file comparison
|
|
func normalizeTimestamps(output, format string) string {
|
|
switch format {
|
|
case "json":
|
|
// Use regex to replace timestamp values with a fixed string
|
|
// This matches ISO8601 timestamps in JSON format
|
|
re := regexp.MustCompile(`"ts":\s*"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]+\+[0-9]{2}:[0-9]{2}"`)
|
|
output = re.ReplaceAllString(output, `"ts": "NORMALIZED_TIMESTAMP"`)
|
|
|
|
// For stats_mixed_json, we need to normalize the order of processes
|
|
// This is a hack, but it works for now
|
|
if strings.Contains(output, `"by_proc"`) {
|
|
// Sort the by_proc array consistently
|
|
lines := strings.Split(output, "\n")
|
|
result := []string{}
|
|
inByProc := false
|
|
byProcLines := []string{}
|
|
|
|
for _, line := range lines {
|
|
if strings.Contains(line, `"by_proc"`) {
|
|
inByProc = true
|
|
result = append(result, line)
|
|
} else if inByProc && strings.Contains(line, `]`) {
|
|
// End of by_proc array
|
|
inByProc = false
|
|
|
|
// Sort by_proc lines by pid
|
|
sort.Strings(byProcLines)
|
|
|
|
// Add sorted lines
|
|
result = append(result, byProcLines...)
|
|
result = append(result, line)
|
|
} else if inByProc {
|
|
// Collect by_proc lines
|
|
byProcLines = append(byProcLines, line)
|
|
} else {
|
|
result = append(result, line)
|
|
}
|
|
}
|
|
|
|
return strings.Join(result, "\n")
|
|
}
|
|
|
|
return output
|
|
case "csv":
|
|
// For CSV, we need to handle the header row differently
|
|
lines := strings.Split(output, "\n")
|
|
result := []string{}
|
|
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "PID,") {
|
|
// Header row, keep as is
|
|
result = append(result, line)
|
|
} else {
|
|
// Data row, normalize if needed
|
|
result = append(result, line)
|
|
}
|
|
}
|
|
return strings.Join(result, "\n")
|
|
}
|
|
return output
|
|
}
|
|
|
|
// TestGoldenFileGeneration tests that we can generate all golden files
|
|
func TestGoldenFileGeneration(t *testing.T) {
|
|
if !*updateGolden {
|
|
t.Skip("Skipping golden file generation (use -update-golden to enable)")
|
|
}
|
|
|
|
goldenDir := filepath.Join("testdata", "golden")
|
|
if err := os.MkdirAll(goldenDir, 0755); err != nil {
|
|
t.Fatalf("Failed to create golden directory: %v", err)
|
|
}
|
|
|
|
// Create a README for the golden files
|
|
readme := `# Golden Files
|
|
|
|
This directory contains golden files for output contract verification tests.
|
|
|
|
These files are automatically generated and should not be edited manually.
|
|
To regenerate them, run:
|
|
|
|
go test ./cmd -update-golden
|
|
|
|
## Files
|
|
|
|
- *_table.golden: Table format output
|
|
- *_json.golden: JSON format output
|
|
- *_csv.golden: CSV format output
|
|
- *_wide.golden: Wide table format output
|
|
- stats_*.golden: Statistics command output
|
|
|
|
Each file represents expected output for specific test scenarios.
|
|
`
|
|
|
|
readmePath := filepath.Join(goldenDir, "README.md")
|
|
if err := os.WriteFile(readmePath, []byte(readme), 0644); err != nil {
|
|
t.Errorf("Failed to write golden README: %v", err)
|
|
}
|
|
}
|