release: version 1.1.2 - Add error callback mechanism and comprehensive test suite
Some checks failed
Build RPM Package / Build RPM Packages (CentOS 7, Rocky 8/9/10) (push) Has been cancelled

Features:
- Add ErrorCallback type for UNIX socket connection error reporting
- Add WithErrorCallback option for UnixSocketWriter configuration
- Add BuilderImpl.WithErrorCallback() for propagating callbacks
- Add consecutive failure tracking in processQueue

Testing (50+ new tests):
- Add integration tests for full pipeline (capture → tlsparse → fingerprint → output)
- Add tests for FileWriter.rotate() and Reopen() log rotation
- Add tests for cleanupExpiredFlows() and cleanupLoop() in TLS parser
- Add tests for extractSNIFromPayload() and extractJA4Hash() helpers
- Add tests for config load error paths (invalid YAML, permission denied)
- Add tests for capture.Run() error conditions
- Add tests for signal handling documentation

Documentation:
- Update architecture.yml with new fields (LogLevel, TLSClientHello extensions)
- Update architecture.yml with Close() methods for Capture and Parser interfaces
- Update RPM spec changelog

Cleanup:
- Remove empty internal/api/ directory

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Jacquin Antoine
2026-03-02 23:24:56 +01:00
parent 6e5addd6d4
commit 23f3012fb1
10 changed files with 2058 additions and 10 deletions

View File

@ -2,8 +2,205 @@ package capture
import (
"testing"
"time"
"ja4sentinel/api"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
)
func TestCaptureImpl_Run_EmptyInterface(t *testing.T) {
c := New()
if c == nil {
t.Fatal("New() returned nil")
}
cfg := api.Config{
Interface: "",
ListenPorts: []uint16{443},
}
out := make(chan api.RawPacket, 10)
err := c.Run(cfg, out)
if err == nil {
t.Error("Run() with empty interface should return error")
}
if err.Error() != "interface cannot be empty" {
t.Errorf("Run() error = %v, want 'interface cannot be empty'", err)
}
}
func TestCaptureImpl_Run_NonExistentInterface(t *testing.T) {
c := New()
if c == nil {
t.Fatal("New() returned nil")
}
cfg := api.Config{
Interface: "nonexistent_interface_xyz123",
ListenPorts: []uint16{443},
}
out := make(chan api.RawPacket, 10)
err := c.Run(cfg, out)
if err == nil {
t.Error("Run() with non-existent interface should return error")
}
}
func TestCaptureImpl_Run_InvalidBPFFilter(t *testing.T) {
// Get a real interface name
ifaces, err := pcap.FindAllDevs()
if err != nil || len(ifaces) == 0 {
t.Skip("No network interfaces available for testing")
}
c := New()
cfg := api.Config{
Interface: ifaces[0].Name,
ListenPorts: []uint16{443},
BPFFilter: "invalid; rm -rf /", // Invalid characters
}
out := make(chan api.RawPacket, 10)
err = c.Run(cfg, out)
if err == nil {
t.Error("Run() with invalid BPF filter should return error")
}
}
func TestCaptureImpl_Run_ChannelFull_DropsPackets(t *testing.T) {
// This test verifies that when the output channel is full,
// packets are dropped gracefully (non-blocking write)
// We can't easily test the full Run() loop without real interfaces,
// but we can verify the channel behavior with a small buffer
out := make(chan api.RawPacket, 1)
// Fill the channel
out <- api.RawPacket{Data: []byte{1, 2, 3}, Timestamp: time.Now().UnixNano()}
// Channel should be full now, select default should trigger
done := make(chan bool)
go func() {
select {
case out <- api.RawPacket{Data: []byte{4, 5, 6}, Timestamp: time.Now().UnixNano()}:
done <- false // Would block
default:
done <- true // Dropped as expected
}
}()
dropped := <-done
if !dropped {
t.Error("Expected packet to be dropped when channel is full")
}
}
func TestPacketToRawPacket(t *testing.T) {
t.Run("valid_packet", func(t *testing.T) {
// Create a simple TCP packet
eth := layers.Ethernet{
SrcMAC: []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55},
DstMAC: []byte{0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB},
EthernetType: layers.EthernetTypeIPv4,
}
ip := layers.IPv4{
Version: 4,
TTL: 64,
Protocol: layers.IPProtocolTCP,
SrcIP: []byte{192, 168, 1, 1},
DstIP: []byte{10, 0, 0, 1},
}
tcp := layers.TCP{
SrcPort: 12345,
DstPort: 443,
}
tcp.SetNetworkLayerForChecksum(&ip)
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{}
gopacket.SerializeLayers(buf, opts, &eth, &ip, &tcp)
packet := gopacket.NewPacket(buf.Bytes(), layers.LinkTypeEthernet, gopacket.Default)
rawPkt := packetToRawPacket(packet)
if rawPkt == nil {
t.Fatal("packetToRawPacket() returned nil for valid packet")
}
if len(rawPkt.Data) == 0 {
t.Error("packetToRawPacket() returned empty data")
}
if rawPkt.Timestamp == 0 {
t.Error("packetToRawPacket() returned zero timestamp")
}
})
t.Run("empty_packet", func(t *testing.T) {
// Create packet with no data
packet := gopacket.NewPacket([]byte{}, layers.LinkTypeEthernet, gopacket.Default)
rawPkt := packetToRawPacket(packet)
if rawPkt != nil {
t.Error("packetToRawPacket() should return nil for empty packet")
}
})
t.Run("nil_packet", func(t *testing.T) {
// packetToRawPacket will panic with nil packet due to Metadata() call
// This is expected behavior - the function is not designed to handle nil
defer func() {
if r := recover(); r == nil {
t.Error("packetToRawPacket() with nil packet should panic")
}
}()
var packet gopacket.Packet
_ = packetToRawPacket(packet)
})
}
func TestGetInterfaceNames(t *testing.T) {
t.Run("empty_list", func(t *testing.T) {
names := getInterfaceNames([]pcap.Interface{})
if len(names) != 0 {
t.Errorf("getInterfaceNames() with empty list = %v, want []", names)
}
})
t.Run("single_interface", func(t *testing.T) {
ifaces := []pcap.Interface{
{Name: "eth0"},
}
names := getInterfaceNames(ifaces)
if len(names) != 1 || names[0] != "eth0" {
t.Errorf("getInterfaceNames() = %v, want [eth0]", names)
}
})
t.Run("multiple_interfaces", func(t *testing.T) {
ifaces := []pcap.Interface{
{Name: "eth0"},
{Name: "lo"},
{Name: "docker0"},
}
names := getInterfaceNames(ifaces)
if len(names) != 3 {
t.Errorf("getInterfaceNames() returned %d names, want 3", len(names))
}
expected := []string{"eth0", "lo", "docker0"}
for i, name := range names {
if name != expected[i] {
t.Errorf("getInterfaceNames()[%d] = %s, want %s", i, name, expected[i])
}
}
})
}
func TestValidateBPFFilter(t *testing.T) {
tests := []struct {
name string