diff --git a/api/types_test.go b/api/types_test.go new file mode 100644 index 0000000..fc3e7c3 --- /dev/null +++ b/api/types_test.go @@ -0,0 +1,269 @@ +package api + +import ( + "testing" +) + +func TestNewLogRecord(t *testing.T) { + tests := []struct { + name string + clientHello TLSClientHello + fingerprints *Fingerprints + wantNil bool + }{ + { + name: "complete record with fingerprints", + clientHello: TLSClientHello{ + SrcIP: "192.168.1.100", + SrcPort: 54321, + DstIP: "10.0.0.1", + DstPort: 443, + IPMeta: IPMeta{ + TTL: 64, + TotalLength: 512, + IPID: 12345, + DF: true, + }, + TCPMeta: TCPMeta{ + WindowSize: 65535, + MSS: 1460, + WindowScale: 7, + Options: []string{"MSS", "WS", "SACK", "TS"}, + }, + }, + fingerprints: &Fingerprints{ + JA4: "t13d1516h2_8daaf6152771_02cb136f2775", + JA4Hash: "8daaf6152771_02cb136f2775", + JA3: "771,4865-4866-4867,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0", + JA3Hash: "a0e6f06c7a6d15e5e3f0f0e6f06c7a6d", + }, + wantNil: false, + }, + { + name: "record without fingerprints", + clientHello: TLSClientHello{ + SrcIP: "192.168.1.100", + SrcPort: 54321, + DstIP: "10.0.0.1", + DstPort: 443, + IPMeta: IPMeta{ + TTL: 64, + TotalLength: 512, + IPID: 12345, + DF: true, + }, + TCPMeta: TCPMeta{ + WindowSize: 65535, + MSS: 1460, + WindowScale: 7, + Options: []string{"MSS", "WS"}, + }, + }, + fingerprints: nil, + wantNil: false, + }, + { + name: "record with zero values for optional fields", + clientHello: TLSClientHello{ + SrcIP: "192.168.1.100", + SrcPort: 54321, + DstIP: "10.0.0.1", + DstPort: 443, + IPMeta: IPMeta{ + TTL: 0, + TotalLength: 0, + IPID: 0, + DF: false, + }, + TCPMeta: TCPMeta{ + WindowSize: 0, + MSS: 0, + WindowScale: 0, + Options: []string{}, + }, + }, + fingerprints: nil, + wantNil: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rec := NewLogRecord(tt.clientHello, tt.fingerprints) + + // Verify basic fields + if rec.SrcIP != tt.clientHello.SrcIP { + t.Errorf("SrcIP = %v, want %v", rec.SrcIP, tt.clientHello.SrcIP) + } + if rec.SrcPort != tt.clientHello.SrcPort { + t.Errorf("SrcPort = %v, want %v", rec.SrcPort, tt.clientHello.SrcPort) + } + if rec.DstIP != tt.clientHello.DstIP { + t.Errorf("DstIP = %v, want %v", rec.DstIP, tt.clientHello.DstIP) + } + if rec.DstPort != tt.clientHello.DstPort { + t.Errorf("DstPort = %v, want %v", rec.DstPort, tt.clientHello.DstPort) + } + + // Verify IPMeta fields + if rec.IPTTL != tt.clientHello.IPMeta.TTL { + t.Errorf("IPTTL = %v, want %v", rec.IPTTL, tt.clientHello.IPMeta.TTL) + } + if rec.IPTotalLen != tt.clientHello.IPMeta.TotalLength { + t.Errorf("IPTotalLen = %v, want %v", rec.IPTotalLen, tt.clientHello.IPMeta.TotalLength) + } + if rec.IPID != tt.clientHello.IPMeta.IPID { + t.Errorf("IPID = %v, want %v", rec.IPID, tt.clientHello.IPMeta.IPID) + } + if rec.IPDF != tt.clientHello.IPMeta.DF { + t.Errorf("IPDF = %v, want %v", rec.IPDF, tt.clientHello.IPMeta.DF) + } + + // Verify TCPMeta fields + if rec.TCPWindow != tt.clientHello.TCPMeta.WindowSize { + t.Errorf("TCPWindow = %v, want %v", rec.TCPWindow, tt.clientHello.TCPMeta.WindowSize) + } + + // Verify optional fields (MSS, WindowScale) + if tt.clientHello.TCPMeta.MSS != 0 { + if rec.TCPMSS == nil { + t.Error("TCPMSS should not be nil when MSS != 0") + } else if *rec.TCPMSS != tt.clientHello.TCPMeta.MSS { + t.Errorf("TCPMSS = %v, want %v", *rec.TCPMSS, tt.clientHello.TCPMeta.MSS) + } + } else { + if rec.TCPMSS != nil { + t.Error("TCPMSS should be nil when MSS == 0") + } + } + + if tt.clientHello.TCPMeta.WindowScale != 0 { + if rec.TCPWScale == nil { + t.Error("TCPWScale should not be nil when WindowScale != 0") + } else if *rec.TCPWScale != tt.clientHello.TCPMeta.WindowScale { + t.Errorf("TCPWScale = %v, want %v", *rec.TCPWScale, tt.clientHello.TCPMeta.WindowScale) + } + } else { + if rec.TCPWScale != nil { + t.Error("TCPWScale should be nil when WindowScale == 0") + } + } + + // Verify fingerprints + if tt.fingerprints != nil { + if rec.JA4 != tt.fingerprints.JA4 { + t.Errorf("JA4 = %v, want %v", rec.JA4, tt.fingerprints.JA4) + } + if rec.JA4Hash != tt.fingerprints.JA4Hash { + t.Errorf("JA4Hash = %v, want %v", rec.JA4Hash, tt.fingerprints.JA4Hash) + } + if rec.JA3 != tt.fingerprints.JA3 { + t.Errorf("JA3 = %v, want %v", rec.JA3, tt.fingerprints.JA3) + } + if rec.JA3Hash != tt.fingerprints.JA3Hash { + t.Errorf("JA3Hash = %v, want %v", rec.JA3Hash, tt.fingerprints.JA3Hash) + } + } else { + if rec.JA4 != "" { + t.Error("JA4 should be empty when fingerprints is nil") + } + } + }) + } +} + +func TestDefaultConfig(t *testing.T) { + cfg := DefaultConfig() + + if cfg.Core.Interface != DefaultInterface { + t.Errorf("Core.Interface = %v, want %v", cfg.Core.Interface, DefaultInterface) + } + if len(cfg.Core.ListenPorts) != 1 { + t.Errorf("Core.ListenPorts length = %v, want 1", len(cfg.Core.ListenPorts)) + } + if cfg.Core.ListenPorts[0] != DefaultPort { + t.Errorf("Core.ListenPorts[0] = %v, want %v", cfg.Core.ListenPorts[0], DefaultPort) + } + if cfg.Core.BPFFilter != DefaultBPFFilter { + t.Errorf("Core.BPFFilter = %v, want %v", cfg.Core.BPFFilter, DefaultBPFFilter) + } + if cfg.Core.FlowTimeoutSec != DefaultFlowTimeout { + t.Errorf("Core.FlowTimeoutSec = %v, want %v", cfg.Core.FlowTimeoutSec, DefaultFlowTimeout) + } + if len(cfg.Outputs) != 0 { + t.Errorf("Outputs length = %v, want 0", len(cfg.Outputs)) + } +} + +func TestJoinStringSlice(t *testing.T) { + tests := []struct { + name string + slice []string + sep string + want string + }{ + { + name: "empty slice", + slice: []string{}, + sep: ",", + want: "", + }, + { + name: "nil slice", + slice: nil, + sep: ",", + want: "", + }, + { + name: "single element", + slice: []string{"hello"}, + sep: ",", + want: "hello", + }, + { + name: "multiple elements", + slice: []string{"MSS", "WS", "SACK", "TS"}, + sep: ",", + want: "MSS,WS,SACK,TS", + }, + { + name: "multiple elements with multi-char separator", + slice: []string{"MSS", "WS", "SACK"}, + sep: ", ", + want: "MSS, WS, SACK", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := joinStringSlice(tt.slice, tt.sep) + if got != tt.want { + t.Errorf("joinStringSlice() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLogRecordConversion(t *testing.T) { + // Test that NewLogRecord correctly converts TCPMeta options to comma-separated string + clientHello := TLSClientHello{ + SrcIP: "192.168.1.100", + SrcPort: 54321, + DstIP: "10.0.0.1", + DstPort: 443, + TCPMeta: TCPMeta{ + WindowSize: 65535, + MSS: 1460, + WindowScale: 7, + Options: []string{"MSS", "WS", "SACK", "TS"}, + }, + } + + rec := NewLogRecord(clientHello, nil) + + // Verify options are joined with comma + expectedOpts := "MSS,WS,SACK,TS" + if rec.TCPOptions != expectedOpts { + t.Errorf("TCPOptions = %v, want %v", rec.TCPOptions, expectedOpts) + } +}