test(api): add unit tests for types.go helper functions
- Add TestNewLogRecord covering complete records, nil fingerprints, and zero values - Add TestDefaultConfig verifying default configuration values - Add TestJoinStringSlice testing edge cases (empty, nil, single, multiple elements) - Add TestLogRecordConversion verifying TCP options formatting Implements testing.policy.requirements.test_skeletons from architecture.yml Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
269
api/types_test.go
Normal file
269
api/types_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user