package fingerprint import ( "testing" "ja4sentinel/api" ) func TestFromClientHello(t *testing.T) { tests := []struct { name string ch api.TLSClientHello wantErr bool }{ { name: "empty payload", ch: api.TLSClientHello{ Payload: []byte{}, }, wantErr: true, }, { name: "invalid payload", ch: api.TLSClientHello{ Payload: []byte{0x00, 0x01, 0x02}, }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { engine := NewEngine() _, err := engine.FromClientHello(tt.ch) if (err != nil) != tt.wantErr { t.Errorf("FromClientHello() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestNewEngine(t *testing.T) { engine := NewEngine() if engine == nil { t.Error("NewEngine() returned nil") } } func TestFromClientHello_ValidPayload(t *testing.T) { // Use a minimal valid TLS 1.2 ClientHello with extensions // Build a proper ClientHello using the same structure as parser tests clientHello := buildMinimalClientHelloForTest() ch := api.TLSClientHello{ SrcIP: "192.168.1.100", SrcPort: 54321, DstIP: "10.0.0.1", DstPort: 443, Payload: clientHello, } engine := NewEngine() fp, err := engine.FromClientHello(ch) if err != nil { t.Fatalf("FromClientHello() error = %v", err) } if fp == nil { t.Fatal("FromClientHello() returned nil") } // Verify JA4 is populated (format: t13d... or t12d...) if fp.JA4 == "" { t.Error("JA4 should not be empty") } // JA4Hash is populated for internal use (but not serialized to LogRecord) // It contains the hash portions of the JA4 string if fp.JA4Hash == "" { t.Error("JA4Hash should be populated for internal use") } } // buildMinimalClientHelloForTest creates a minimal valid TLS 1.2 ClientHello func buildMinimalClientHelloForTest() []byte { // Cipher suites (minimal set) cipherSuites := []byte{0x00, 0x04, 0x13, 0x01, 0x13, 0x02, 0xc0, 0x2f} // Compression methods (null only) compressionMethods := []byte{0x01, 0x00} // No extensions extensions := []byte{} extLen := len(extensions) // Build ClientHello handshake body handshakeBody := []byte{ 0x03, 0x03, // Version: TLS 1.2 // Random (32 bytes) 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Session ID length: 0 } // Add cipher suites (with length prefix) cipherSuiteLen := len(cipherSuites) handshakeBody = append(handshakeBody, byte(cipherSuiteLen>>8), byte(cipherSuiteLen)) handshakeBody = append(handshakeBody, cipherSuites...) // Add compression methods (with length prefix) handshakeBody = append(handshakeBody, compressionMethods...) // Add extensions (with length prefix) handshakeBody = append(handshakeBody, byte(extLen>>8), byte(extLen)) handshakeBody = append(handshakeBody, extensions...) // Now build full handshake with type and length handshakeLen := len(handshakeBody) handshake := append([]byte{ 0x01, // Handshake type: ClientHello byte(handshakeLen >> 16), byte(handshakeLen >> 8), byte(handshakeLen), // Handshake length }, handshakeBody...) // Build TLS record recordLen := len(handshake) record := make([]byte, 5+recordLen) record[0] = 0x16 // Handshake record[1] = 0x03 // Version: TLS 1.2 record[2] = 0x03 record[3] = byte(recordLen >> 8) record[4] = byte(recordLen) copy(record[5:], handshake) return record }