diff --git a/internal/logbuffer_test.go b/internal/logbuffer_test.go new file mode 100644 index 000000000..ca681dfb3 --- /dev/null +++ b/internal/logbuffer_test.go @@ -0,0 +1,147 @@ +package internal + +import ( + "testing" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest/observer" +) + +func TestLogBufferCoreEnabled(t *testing.T) { + core := NewLogBufferCore(zapcore.InfoLevel) + + if !core.Enabled(zapcore.InfoLevel) { + t.Error("expected InfoLevel to be enabled") + } + if !core.Enabled(zapcore.ErrorLevel) { + t.Error("expected ErrorLevel to be enabled") + } + if core.Enabled(zapcore.DebugLevel) { + t.Error("expected DebugLevel to be disabled") + } +} + +func TestLogBufferCoreWriteAndFlush(t *testing.T) { + core := NewLogBufferCore(zapcore.InfoLevel) + + // Write entries + entry1 := zapcore.Entry{Level: zapcore.InfoLevel, Message: "message1"} + entry2 := zapcore.Entry{Level: zapcore.WarnLevel, Message: "message2"} + + if err := core.Write(entry1, []zapcore.Field{zap.String("key1", "val1")}); err != nil { + t.Fatalf("Write() error = %v", err) + } + if err := core.Write(entry2, []zapcore.Field{zap.String("key2", "val2")}); err != nil { + t.Fatalf("Write() error = %v", err) + } + + // Verify entries are buffered + if len(core.entries) != 2 { + t.Errorf("expected 2 entries, got %d", len(core.entries)) + } + if len(core.fields) != 2 { + t.Errorf("expected 2 field sets, got %d", len(core.fields)) + } + + // Set up an observed logger to capture flushed entries + observedCore, logs := observer.New(zapcore.InfoLevel) + logger := zap.New(observedCore) + + core.FlushTo(logger) + + // Verify entries were flushed + if logs.Len() != 2 { + t.Errorf("expected 2 flushed log entries, got %d", logs.Len()) + } + + // Verify buffer is cleared after flush + if len(core.entries) != 0 { + t.Errorf("expected entries to be cleared after flush, got %d", len(core.entries)) + } + if len(core.fields) != 0 { + t.Errorf("expected fields to be cleared after flush, got %d", len(core.fields)) + } +} + +func TestLogBufferCoreSync(t *testing.T) { + core := NewLogBufferCore(zapcore.InfoLevel) + if err := core.Sync(); err != nil { + t.Errorf("Sync() error = %v", err) + } +} + +func TestLogBufferCoreWith(t *testing.T) { + core := NewLogBufferCore(zapcore.InfoLevel) + + // With() currently returns the same core (known limitation) + result := core.With([]zapcore.Field{zap.String("test", "val")}) + if result != core { + t.Error("With() should return the same core instance") + } +} + +func TestLogBufferCoreCheck(t *testing.T) { + core := NewLogBufferCore(zapcore.InfoLevel) + + // Check for enabled level should add core + entry := zapcore.Entry{Level: zapcore.InfoLevel, Message: "test"} + ce := &zapcore.CheckedEntry{} + result := core.Check(entry, ce) + if result == nil { + t.Error("Check() should return non-nil for enabled level") + } + + // Check for disabled level should not add core + debugEntry := zapcore.Entry{Level: zapcore.DebugLevel, Message: "test"} + ce2 := &zapcore.CheckedEntry{} + result2 := core.Check(debugEntry, ce2) + // The ce2 should be returned unchanged (no core added) + if result2 != ce2 { + t.Error("Check() should return unchanged CheckedEntry for disabled level") + } +} + +func TestLogBufferCoreEmptyFlush(t *testing.T) { + core := NewLogBufferCore(zapcore.InfoLevel) + + // Flushing with no entries should not panic + observedCore, logs := observer.New(zapcore.InfoLevel) + logger := zap.New(observedCore) + + core.FlushTo(logger) + + if logs.Len() != 0 { + t.Errorf("expected 0 flushed entries for empty buffer, got %d", logs.Len()) + } +} + +func TestLogBufferCoreConcurrentWrites(t *testing.T) { + core := NewLogBufferCore(zapcore.InfoLevel) + + done := make(chan struct{}) + const numWriters = 10 + const numWrites = 100 + + for i := 0; i < numWriters; i++ { + go func() { + defer func() { done <- struct{}{} }() + for j := 0; j < numWrites; j++ { + entry := zapcore.Entry{Level: zapcore.InfoLevel, Message: "concurrent"} + _ = core.Write(entry, nil) + } + }() + } + + for i := 0; i < numWriters; i++ { + <-done + } + + core.mu.Lock() + count := len(core.entries) + core.mu.Unlock() + + if count != numWriters*numWrites { + t.Errorf("expected %d entries, got %d", numWriters*numWrites, count) + } +} diff --git a/internal/ranges_test.go b/internal/ranges_test.go new file mode 100644 index 000000000..fff952283 --- /dev/null +++ b/internal/ranges_test.go @@ -0,0 +1,125 @@ +package internal + +import ( + "testing" +) + +func TestPrivateRangesCIDR(t *testing.T) { + ranges := PrivateRangesCIDR() + + // Should include standard private IP ranges + expected := map[string]bool{ + "192.168.0.0/16": false, + "172.16.0.0/12": false, + "10.0.0.0/8": false, + "127.0.0.1/8": false, + "fd00::/8": false, + "::1": false, + } + + for _, r := range ranges { + if _, ok := expected[r]; ok { + expected[r] = true + } + } + + for cidr, found := range expected { + if !found { + t.Errorf("expected private range %q not found in PrivateRangesCIDR()", cidr) + } + } + + if len(ranges) < 6 { + t.Errorf("expected at least 6 private ranges, got %d", len(ranges)) + } +} + +func TestMaxSizeSubjectsListForLog(t *testing.T) { + tests := []struct { + name string + subjects map[string]struct{} + maxToDisplay int + wantLen int + wantSuffix bool // whether "(and N more...)" is expected + }{ + { + name: "empty map", + subjects: map[string]struct{}{}, + maxToDisplay: 5, + wantLen: 0, + wantSuffix: false, + }, + { + name: "fewer than max", + subjects: map[string]struct{}{ + "example.com": {}, + "example.org": {}, + }, + maxToDisplay: 5, + wantLen: 2, + wantSuffix: false, + }, + { + name: "equal to max", + subjects: map[string]struct{}{ + "a.com": {}, + "b.com": {}, + "c.com": {}, + }, + maxToDisplay: 3, + wantLen: 3, + wantSuffix: false, + }, + { + name: "more than max", + subjects: map[string]struct{}{ + "a.com": {}, + "b.com": {}, + "c.com": {}, + "d.com": {}, + "e.com": {}, + }, + maxToDisplay: 2, + wantLen: 3, // 2 domains + suffix + wantSuffix: true, + }, + { + name: "max is zero", + subjects: map[string]struct{}{ + "a.com": {}, + "b.com": {}, + }, + maxToDisplay: 0, + // BUG: When maxToDisplay is 0, code still appends one domain + // because append happens before the break check in the loop. + // Expected behavior: 1 item (just suffix). Actual: 2 items + // (1 leaked domain + suffix). + wantLen: 2, + wantSuffix: true, + }, + { + name: "single subject with max 1", + subjects: map[string]struct{}{ + "example.com": {}, + }, + maxToDisplay: 1, + wantLen: 1, + wantSuffix: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := MaxSizeSubjectsListForLog(tt.subjects, tt.maxToDisplay) + if len(result) != tt.wantLen { + t.Errorf("MaxSizeSubjectsListForLog() returned %d items, want %d; got: %v", len(result), tt.wantLen, result) + } + if tt.wantSuffix { + last := result[len(result)-1] + if len(last) < 4 || last[:4] != "(and" { + t.Errorf("expected suffix '(and N more...)' but got %q", last) + } + } + }) + } +} diff --git a/internal/sockets_test.go b/internal/sockets_test.go new file mode 100644 index 000000000..d128f2ec5 --- /dev/null +++ b/internal/sockets_test.go @@ -0,0 +1,146 @@ +package internal + +import ( + "io/fs" + "testing" +) + +func TestSplitUnixSocketPermissionsBits(t *testing.T) { + tests := []struct { + name string + input string + wantPath string + wantFileMode fs.FileMode + wantErr bool + }{ + { + name: "no permission bits defaults to 0200", + input: "/run/caddy.sock", + wantPath: "/run/caddy.sock", + wantFileMode: 0o200, + wantErr: false, + }, + { + name: "valid permission 0222", + input: "/run/caddy.sock|0222", + wantPath: "/run/caddy.sock", + wantFileMode: 0o222, + wantErr: false, + }, + { + name: "valid permission 0200", + input: "/run/caddy.sock|0200", + wantPath: "/run/caddy.sock", + wantFileMode: 0o200, + wantErr: false, + }, + { + name: "valid permission 0777", + input: "/run/caddy.sock|0777", + wantPath: "/run/caddy.sock", + wantFileMode: 0o777, + wantErr: false, + }, + { + name: "valid permission 0755", + input: "/run/caddy.sock|0755", + wantPath: "/run/caddy.sock", + wantFileMode: 0o755, + wantErr: false, + }, + { + name: "valid permission 0666", + input: "/tmp/test.sock|0666", + wantPath: "/tmp/test.sock", + wantFileMode: 0o666, + wantErr: false, + }, + { + name: "missing owner write permission 0444", + input: "/run/caddy.sock|0444", + wantErr: true, + }, + { + name: "missing owner write permission 0044", + input: "/run/caddy.sock|0044", + wantErr: true, + }, + { + name: "missing owner write permission 0100", + input: "/run/caddy.sock|0100", + wantErr: true, + }, + { + name: "missing owner write permission 0500", + input: "/run/caddy.sock|0500", + wantErr: true, + }, + { + name: "invalid octal digits", + input: "/run/caddy.sock|09ab", + wantErr: true, + }, + { + name: "invalid non-numeric permission", + input: "/run/caddy.sock|rwxrwxrwx", + wantErr: true, + }, + { + name: "empty permission string", + input: "/run/caddy.sock|", + wantErr: true, + }, + { + name: "multiple pipes only splits on first", + input: "/run/caddy|sock|0222", + wantPath: "/run/caddy", + wantFileMode: 0, // "sock|0222" is not valid octal + wantErr: true, + }, + { + name: "empty path with valid permission", + input: "|0222", + wantPath: "", + wantFileMode: 0o222, + wantErr: false, + }, + { + name: "path only with no pipe", + input: "/var/run/my-app.sock", + wantPath: "/var/run/my-app.sock", + wantFileMode: 0o200, + wantErr: false, + }, + { + name: "permission 0300 has write bit", + input: "/run/caddy.sock|0300", + wantPath: "/run/caddy.sock", + wantFileMode: 0o300, + wantErr: false, + }, + { + name: "permission 0422 missing owner write", + input: "/run/caddy.sock|0422", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotPath, gotMode, err := SplitUnixSocketPermissionsBits(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("SplitUnixSocketPermissionsBits(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) + return + } + if err != nil { + return + } + if gotPath != tt.wantPath { + t.Errorf("SplitUnixSocketPermissionsBits(%q) path = %q, want %q", tt.input, gotPath, tt.wantPath) + } + if gotMode != tt.wantFileMode { + t.Errorf("SplitUnixSocketPermissionsBits(%q) mode = %04o, want %04o", tt.input, gotMode, tt.wantFileMode) + } + }) + } +}