package parser_test import ( "testing" "github.com/antitbone/ja4/ja4ebpf/internal/parser" "golang.org/x/net/http2" ) func TestDetectH2PrefaceTrue(t *testing.T) { preface := []byte("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") data := append(preface, 0x00, 0x00) // données supplémentaires if !parser.DetectH2Preface(data) { t.Error("H2Magic non détecté dans un buffer valide") } } func TestDetectH2PrefaceFalse(t *testing.T) { if parser.DetectH2Preface([]byte("GET / HTTP/1.1\r\n")) { t.Error("détection faux positif HTTP/1.1 comme HTTP/2") } } func TestDetectH2PrefaceTooShort(t *testing.T) { if parser.DetectH2Preface([]byte("PRI *")) { t.Error("détection sur buffer trop court") } } func TestH2MagicPrefaceLen(t *testing.T) { if parser.H2MagicPrefaceLen() != 24 { t.Errorf("longueur préambule HTTP/2 attendue 24, obtenue %d", parser.H2MagicPrefaceLen()) } } // buildH2Frame construit une frame HTTP/2 brute (en-tête 9 octets + payload). func buildH2Frame(frameType, flags uint8, streamID uint32, payload []byte) []byte { l := len(payload) frame := []byte{ byte(l >> 16), byte(l >> 8), byte(l), // longueur sur 3 octets frameType, flags, byte(streamID >> 24), byte(streamID >> 16), byte(streamID >> 8), byte(streamID), } return append(frame, payload...) } // TestH2ConnStateSettings verifies that H2ConnState processes SETTINGS frames correctly. func TestH2ConnStateSettings(t *testing.T) { conn := parser.NewH2ConnState() // SETTINGS frame with HEADER_TABLE_SIZE=4096, INITIAL_WINDOW_SIZE=65535 settingsPayload := []byte{ 0x00, 0x01, 0x00, 0x00, 0x10, 0x00, // HEADER_TABLE_SIZE = 4096 0x00, 0x04, 0x00, 0x00, 0xff, 0xff, // INITIAL_WINDOW_SIZE = 65535 } frame := buildH2Frame(0x4, 0x0, 0, settingsPayload) // SETTINGS, no flags, stream 0 result, err := conn.ProcessFrames(frame, 0) if err != nil { t.Fatalf("ProcessFrames: %v", err) } if result == nil { t.Fatal("result ne doit pas être nil") } if result.ClientSettings == nil { t.Fatal("ClientSettings ne doit pas être nil") } if result.ClientSettings.HeaderTableSize != 4096 { t.Errorf("HeaderTableSize: attendu 4096, obtenu %d", result.ClientSettings.HeaderTableSize) } if result.ClientSettings.InitialWindowSize != 65535 { t.Errorf("InitialWindowSize: attendu 65535, obtenu %d", result.ClientSettings.InitialWindowSize) } } // TestH2ConnStateWindowUpdate verifies WINDOW_UPDATE on stream 0. func TestH2ConnStateWindowUpdate(t *testing.T) { conn := parser.NewH2ConnState() // WINDOW_UPDATE on stream 0 with increment = 1073741824 (0x40000000) wuPayload := []byte{0x40, 0x00, 0x00, 0x00} frame := buildH2Frame(0x8, 0x0, 0, wuPayload) // WINDOW_UPDATE, stream 0 result, err := conn.ProcessFrames(frame, 0) if err != nil { t.Fatalf("ProcessFrames: %v", err) } if result == nil { t.Fatal("result ne doit pas être nil") } if result.ConnWindowUpdate != 1073741824 { t.Errorf("WindowUpdateIncrement: attendu 1073741824, obtenu %d", result.ConnWindowUpdate) } } // TestH2ConnStateHeadersWithHPACK verifies HEADERS frame decoding via hpack.Decoder. func TestH2ConnStateHeadersWithHPACK(t *testing.T) { conn := parser.NewH2ConnState() // HEADERS frame with END_HEADERS flag: // 0x82 = :method GET (indexed) // 0x84 = :path / (indexed) // 0x41 = :authority with literal value "example.com" headersPayload := []byte{ 0x82, // :method GET 0x84, // :path / 0x41, // :authority with literal value 0x0B, 'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm', } frame := buildH2Frame(0x1, 0x04, 1, headersPayload) // HEADERS, END_HEADERS, stream 1 result, err := conn.ProcessFrames(frame, 0) if err != nil { t.Fatalf("ProcessFrames: %v", err) } if result == nil { t.Fatal("result ne doit pas être nil") } // Check headers headerMap := make(map[string]string) for _, h := range result.Headers { headerMap[h.Name] = h.Value } if headerMap[":method"] != "GET" { t.Errorf(":method: attendu 'GET', obtenu %q", headerMap[":method"]) } if headerMap[":path"] != "/" { t.Errorf(":path: attendu '/', obtenu %q", headerMap[":path"]) } if headerMap[":authority"] != "example.com" { t.Errorf(":authority: attendu 'example.com', obtenu %q", headerMap[":authority"]) } } // TestH2ConnStateHeadersFullyIndexed verifies fully-indexed HPACK representations. func TestH2ConnStateHeadersFullyIndexed(t *testing.T) { conn := parser.NewH2ConnState() // All fully-indexed: :method GET, :scheme https, :path /, accept */* // Note: Go's hpack static table has index 19 as accept="" (no default value), // unlike RFC 7541 which defines it as accept: */*. We test actual Go behavior. headersPayload := []byte{ 0x82, // :method GET 0x87, // :scheme https 0x84, // :path / 0x93, // accept (Go hpack: empty value; RFC 7541: */*) } frame := buildH2Frame(0x1, 0x04, 1, headersPayload) result, err := conn.ProcessFrames(frame, 0) if err != nil { t.Fatalf("ProcessFrames: %v", err) } headerMap := make(map[string]string) for _, h := range result.Headers { headerMap[h.Name] = h.Value } if headerMap[":method"] != "GET" { t.Errorf(":method: attendu 'GET', obtenu %q", headerMap[":method"]) } if headerMap[":scheme"] != "https" { t.Errorf(":scheme: attendu 'https', obtenu %q", headerMap[":scheme"]) } if headerMap[":path"] != "/" { t.Errorf(":path: attendu '/', obtenu %q", headerMap[":path"]) } // Go's hpack emits accept="" for index 19 — verify it's present but empty if _, ok := headerMap["accept"]; !ok { t.Error("accept: header attendu mais absent") } } // TestH2ConnStatePrefaceAndSettings verifies processing of H2 preface followed by SETTINGS. func TestH2ConnStatePrefaceAndSettings(t *testing.T) { // Client preface: magic + SETTINGS frame preface := []byte("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") // SETTINGS with INITIAL_WINDOW_SIZE=65536 and MAX_CONCURRENT_STREAMS=100 settingsPayload := []byte{ 0x00, 0x04, 0x00, 0x00, 0xff, 0xff, // INITIAL_WINDOW_SIZE = 65535 0x00, 0x03, 0x00, 0x00, 0x00, 0x64, // MAX_CONCURRENT_STREAMS = 100 } settingsFrame := buildH2Frame(0x4, 0x0, 0, settingsPayload) data := append(preface, settingsFrame...) // Detect preface and process remaining bytes afterPreface := data[parser.H2MagicPrefaceLen():] conn := parser.NewH2ConnState() result, err := conn.ProcessFrames(afterPreface, 0) if err != nil { t.Fatalf("ProcessFrames: %v", err) } if result == nil || result.ClientSettings == nil { t.Fatal("ClientSettings ne doit pas être nil") } if result.ClientSettings.InitialWindowSize != 65535 { t.Errorf("InitialWindowSize: attendu 65535, obtenu %d", result.ClientSettings.InitialWindowSize) } if result.ClientSettings.MaxConcurrentStreams != 100 { t.Errorf("MaxConcurrentStreams: attendu 100, obtenu %d", result.ClientSettings.MaxConcurrentStreams) } } // TestH2ConnStateDynamicTable verifies that HPACK dynamic table works across multiple HEADERS frames. func TestH2ConnStateDynamicTable(t *testing.T) { conn := parser.NewH2ConnState() // First HEADERS frame: :method GET, :authority example.com (literal with indexing) // This adds "example.com" to the dynamic table headers1 := []byte{ 0x82, // :method GET (indexed) 0x41, // :authority with literal value (indexed in dynamic table) 0x0B, 'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm', } frame1 := buildH2Frame(0x1, 0x04, 1, headers1) result1, _ := conn.ProcessFrames(frame1, 0) if result1 == nil { t.Fatal("result1 ne doit pas être nil") } headerMap1 := make(map[string]string) for _, h := range result1.Headers { headerMap1[h.Name] = h.Value } if headerMap1[":authority"] != "example.com" { t.Errorf("first frame: :authority attendu 'example.com', obtenu %q", headerMap1[":authority"]) } // Second HEADERS frame on stream 3: :method GET, :authority example.com (now in dynamic table) // After adding "example.com" with index 62 in dynamic table, we can reference it // However, for a simple test, we just verify the decoder still works headers2 := []byte{ 0x82, // :method GET (indexed) 0x84, // :path / (indexed) } frame2 := buildH2Frame(0x1, 0x04, 3, headers2) result2, _ := conn.ProcessFrames(frame2, 0) if result2 == nil { t.Fatal("result2 ne doit pas être nil") } headerMap2 := make(map[string]string) for _, h := range result2.Headers { headerMap2[h.Name] = h.Value } if headerMap2[":method"] != "GET" { t.Errorf("second frame: :method attendu 'GET', obtenu %q", headerMap2[":method"]) } if headerMap2[":path"] != "/" { t.Errorf("second frame: :path attendu '/', obtenu %q", headerMap2[":path"]) } } // TestH2ConnStateServerStatus verifies :status extraction from server HEADERS. func TestH2ConnStateServerStatus(t *testing.T) { conn := parser.NewH2ConnState() // Server HEADERS frame with :status 200 (indexed, byte 0x88) headersPayload := []byte{0x88} // :status 200 frame := buildH2Frame(0x1, 0x04, 1, headersPayload) result, err := conn.ProcessFrames(frame, 1) // direction=1 (server→client) if err != nil { t.Fatalf("ProcessFrames: %v", err) } if result.StatusCode != 200 { t.Errorf("StatusCode: attendu 200, obtenu %d", result.StatusCode) } } // TestH2ConnStateGoAway verifies GOAWAY frame processing. func TestH2ConnStateGoAway(t *testing.T) { conn := parser.NewH2ConnState() // GOAWAY frame: last stream ID = 0, error code = NO_ERROR (0) goawayPayload := []byte{ 0x00, 0x00, 0x00, 0x00, // last stream ID = 0 0x00, 0x00, 0x00, 0x00, // error code = NO_ERROR } frame := buildH2Frame(0x7, 0x0, 0, goawayPayload) // GOAWAY, stream 0 result, err := conn.ProcessFrames(frame, 1) if err != nil { t.Fatalf("ProcessFrames: %v", err) } if result.GoAwayLastStream != 0 { t.Errorf("GoAwayLastStream: attendu 0, obtenu %d", result.GoAwayLastStream) } } // TestIsH2FrameHeader verifies frame detection using http2.Framer. func TestIsH2FrameHeader(t *testing.T) { // Frame SETTINGS valide frame := buildH2Frame(0x4, 0x0, 0, []byte{}) if !parser.IsH2FrameHeader(frame) { t.Error("IsH2FrameHeader doit retourner true pour frame SETTINGS valide") } // Données aléatoires random := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} if parser.IsH2FrameHeader(random) { t.Error("IsH2FrameHeader doit retourner false pour données invalides") } // Trop court if parser.IsH2FrameHeader([]byte{0x00, 0x00}) { t.Error("IsH2FrameHeader doit retourner false pour données trop courtes") } } // TestH2ConnStateRSTStream verifies RST_STREAM frame processing. func TestH2ConnStateRSTStream(t *testing.T) { conn := parser.NewH2ConnState() // RST_STREAM on stream 1 with error code CANCEL (0x08) rstPayload := []byte{0x00, 0x00, 0x00, 0x08} // error code CANCEL frame := buildH2Frame(0x3, 0x0, 1, rstPayload) // RST_STREAM, stream 1 result, _ := conn.ProcessFrames(frame, 1) if result == nil { t.Fatal("result ne doit pas être nil") } // Check that stream 1 is in the closed streams found := false for _, id := range result.StreamClosed { if id == 1 { found = true } } if !found { t.Error("stream 1 devrait être dans StreamClosed après RST_STREAM") } } // TestHpackDecoderBasic verifies the hpack.Decoder works correctly via H2ConnState. func TestHpackDecoderBasic(t *testing.T) { // Create an H2ConnState and feed it a SETTINGS frame first (to set dynamic table size) conn := parser.NewH2ConnState() // SETTINGS with HEADER_TABLE_SIZE=4096 settingsPayload := []byte{ 0x00, 0x01, 0x00, 0x00, 0x10, 0x00, // HEADER_TABLE_SIZE = 4096 } settingsFrame := buildH2Frame(0x4, 0x0, 0, settingsPayload) result, _ := conn.ProcessFrames(settingsFrame, 0) if result.ClientSettings == nil || result.ClientSettings.HeaderTableSize != 4096 { t.Errorf("HEADER_TABLE_SIZE: attendu 4096") } // Now feed a HEADERS frame with user-agent (literal with indexed name) // user-agent is index 58 in HPACK static table // 0x40 | 58 = 0x7A, then value length 8, then "curl/8.0" uaPayload := []byte{ 0x82, // :method GET 0x7A, // user-agent with literal value (indexed name 58) 0x08, 'c', 'u', 'r', 'l', '/', '8', '.', '0', } uaFrame := buildH2Frame(0x1, 0x04, 1, uaPayload) result2, _ := conn.ProcessFrames(uaFrame, 0) headerMap := make(map[string]string) for _, h := range result2.Headers { headerMap[h.Name] = h.Value } if headerMap["user-agent"] != "curl/8.0" { t.Errorf("user-agent: attendu 'curl/8.0', obtenu %q", headerMap["user-agent"]) } if headerMap[":method"] != "GET" { t.Errorf(":method: attendu 'GET', obtenu %q", headerMap[":method"]) } } // TestH2ConnStateContinuation verifies HEADERS + CONTINUATION assembly. func TestH2ConnStateContinuation(t *testing.T) { conn := parser.NewH2ConnState() // HEADERS frame WITHOUT END_HEADERS (flags=0x00, stream 1) headersPayload := []byte{ 0x82, // :method GET 0x84, // :path / } headersFrame := buildH2Frame(0x1, 0x00, 1, headersPayload) // HEADERS, NO END_HEADERS // CONTINUATION frame WITH END_HEADERS (flags=0x04, stream 1) contPayload := []byte{ 0x41, // :authority with literal value 0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', } contFrame := buildH2Frame(0x9, 0x04, 1, contPayload) // CONTINUATION, END_HEADERS // Process both frames in one call data := append(headersFrame, contFrame...) result, _ := conn.ProcessFrames(data, 0) if result == nil { t.Fatal("result ne doit pas être nil") } headerMap := make(map[string]string) for _, h := range result.Headers { headerMap[h.Name] = h.Value } if headerMap[":method"] != "GET" { t.Errorf(":method: attendu 'GET', obtenu %q", headerMap[":method"]) } if headerMap[":path"] != "/" { t.Errorf(":path: attendu '/', obtenu %q", headerMap[":path"]) } if headerMap[":authority"] != "example" { t.Errorf(":authority: attendu 'example', obtenu %q", headerMap[":authority"]) } } // TestH2ConnStatePing verifies PING frame counting. func TestH2ConnStatePing(t *testing.T) { conn := parser.NewH2ConnState() // PING frame (8 bytes opaque data) pingPayload := make([]byte, 8) frame := buildH2Frame(0x6, 0x0, 0, pingPayload) // PING, stream 0 result, _ := conn.ProcessFrames(frame, 0) if result == nil { t.Fatal("result ne doit pas être nil") } count, ok := result.FrameCounts[http2.FramePing] if !ok || count != 1 { t.Errorf("PING frame count: attendu 1, obtenu %d", count) } } // --------------------------------------------------------------------------- // Phase 2 tests // --------------------------------------------------------------------------- // TestH2ConnStateSettingsAck verifies SETTINGS ACK detection. func TestH2ConnStateSettingsAck(t *testing.T) { conn := parser.NewH2ConnState() // SETTINGS ACK frame (ACK flag = 0x01, no payload) ackFrame := buildH2Frame(0x4, 0x01, 0, []byte{}) // SETTINGS, ACK flag result, err := conn.ProcessFrames(ackFrame, 0) if err != nil { t.Fatalf("ProcessFrames: %v", err) } if !result.SettingsAckSeen { t.Error("SettingsAckSeen devrait être true après SETTINGS ACK") } if !conn.SettingsAck { t.Error("H2ConnState.SettingsAck devrait être true après SETTINGS ACK") } } // TestH2ConnStatePingAck verifies PING ACK flag distinction. func TestH2ConnStatePingAck(t *testing.T) { conn := parser.NewH2ConnState() // PING ACK frame (ACK flag = 0x01) pingPayload := make([]byte, 8) ackFrame := buildH2Frame(0x6, 0x01, 0, pingPayload) // PING, ACK flag result, err := conn.ProcessFrames(ackFrame, 0) if err != nil { t.Fatalf("ProcessFrames: %v", err) } if !result.PingAckSeen { t.Error("PingAckSeen devrait être true après PING ACK") } // Regular PING should NOT set PingAckSeen conn2 := parser.NewH2ConnState() regularPing := buildH2Frame(0x6, 0x0, 0, pingPayload) // PING, no ACK result2, _ := conn2.ProcessFrames(regularPing, 0) if result2.PingAckSeen { t.Error("PingAckSeen ne devrait pas être true pour un PING régulier") } } // TestH2ConnStatePriority verifies PRIORITY frame decoding. func TestH2ConnStatePriority(t *testing.T) { conn := parser.NewH2ConnState() // Create stream 1 first (HEADERS) headersPayload := []byte{0x82, 0x84} // :method GET, :path / headersFrame := buildH2Frame(0x1, 0x04, 1, headersPayload) conn.ProcessFrames(headersFrame, 0) // PRIORITY frame on stream 1: StreamDep=0, Exclusive=false, Weight=15 // PRIORITY payload: 4 bytes (stream dep + exclusive bit) + 1 byte weight priorityPayload := []byte{ 0x00, 0x00, 0x00, 0x00, // StreamDep=0, Exclusive=false (bit 31 = 0) 0x0F, // Weight=15 } priorityFrame := buildH2Frame(0x2, 0x0, 1, priorityPayload) // PRIORITY, stream 1 _, err := conn.ProcessFrames(priorityFrame, 0) if err != nil { t.Fatalf("ProcessFrames: %v", err) } stream, ok := conn.Streams[1] if !ok { t.Fatal("stream 1 devrait exister") } if stream.Priority == nil { t.Fatal("stream.Priority ne devrait pas être nil après PRIORITY frame") } if stream.Priority.Weight != 15 { t.Errorf("Weight: attendu 15, obtenu %d", stream.Priority.Weight) } if stream.Priority.StreamDep != 0 { t.Errorf("StreamDep: attendu 0, obtenu %d", stream.Priority.StreamDep) } if stream.Priority.Exclusive { t.Error("Exclusive devrait être false") } // Verify frame type history found := false for _, ft := range stream.FrameTypes { if ft == http2.FramePriority { found = true } } if !found { t.Error("PRIORITY devrait être dans FrameTypes du stream") } } // TestH2ConnStatePerStreamWindowUpdate verifies per-stream WINDOW_UPDATE. func TestH2ConnStatePerStreamWindowUpdate(t *testing.T) { conn := parser.NewH2ConnState() // Create stream 3 (client-initiated, odd) headersPayload := []byte{0x82, 0x84} // :method GET, :path / headersFrame := buildH2Frame(0x1, 0x04, 3, headersPayload) conn.ProcessFrames(headersFrame, 0) // WINDOW_UPDATE on stream 3 with increment = 32768 wuPayload := []byte{0x00, 0x00, 0x80, 0x00} // 32768 wuFrame := buildH2Frame(0x8, 0x0, 3, wuPayload) // WINDOW_UPDATE, stream 3 result, err := conn.ProcessFrames(wuFrame, 0) if err != nil { t.Fatalf("ProcessFrames: %v", err) } if result == nil { t.Fatal("result ne doit pas être nil") } stream, ok := conn.Streams[3] if !ok { t.Fatal("stream 3 devrait exister") } if stream.WindowIncr != 32768 { t.Errorf("WindowIncr: attendu 32768, obtenu %d", stream.WindowIncr) } } // TestH2ConnStateFrameChronology verifies H2FrameRecord in results. func TestH2ConnStateFrameChronology(t *testing.T) { conn := parser.NewH2ConnState() // SETTINGS frame settingsPayload := []byte{ 0x00, 0x01, 0x00, 0x00, 0x10, 0x00, // HEADER_TABLE_SIZE = 4096 } settingsFrame := buildH2Frame(0x4, 0x0, 0, settingsPayload) // HEADERS frame headersPayload := []byte{0x82, 0x84} // :method GET, :path / headersFrame := buildH2Frame(0x1, 0x04, 1, headersPayload) // Process both frames in one call data := append(settingsFrame, headersFrame...) result, err := conn.ProcessFrames(data, 0) if err != nil { t.Fatalf("ProcessFrames: %v", err) } if len(result.Frames) != 2 { t.Fatalf("Frames: attendu 2, obtenu %d", len(result.Frames)) } // First frame: SETTINGS f0 := result.Frames[0] if f0.Index != 1 { t.Errorf("Frame[0].Index: attendu 1, obtenu %d", f0.Index) } if f0.Direction != 0 { t.Errorf("Frame[0].Direction: attendu 0, obtenu %d", f0.Direction) } if f0.Type != http2.FrameSettings { t.Errorf("Frame[0].Type: attendu SETTINGS, obtenu %v", f0.Type) } if f0.StreamID != 0 { t.Errorf("Frame[0].StreamID: attendu 0, obtenu %d", f0.StreamID) } // Second frame: HEADERS f1 := result.Frames[1] if f1.Index != 2 { t.Errorf("Frame[1].Index: attendu 2, obtenu %d", f1.Index) } if f1.Type != http2.FrameHeaders { t.Errorf("Frame[1].Type: attendu HEADERS, obtenu %v", f1.Type) } if f1.StreamID != 1 { t.Errorf("Frame[1].StreamID: attendu 1, obtenu %d", f1.StreamID) } } // TestH2ConnStateStreamInitiator verifies stream initiator tracking. func TestH2ConnStateStreamInitiator(t *testing.T) { conn := parser.NewH2ConnState() // Stream 1 (client, odd) h1 := []byte{0x82, 0x84} // :method GET, :path / frame1 := buildH2Frame(0x1, 0x04, 1, h1) conn.ProcessFrames(frame1, 0) // Stream 2 (server, even) — server-initiated push promise h2 := []byte{0x88} // :status 200 frame2 := buildH2Frame(0x1, 0x04, 2, h2) conn.ProcessFrames(frame2, 1) stream1, ok1 := conn.Streams[1] if !ok1 { t.Fatal("stream 1 devrait exister") } if stream1.Initiator != 0 { t.Errorf("stream 1 Initiator: attendu 0 (client), obtenu %d", stream1.Initiator) } stream2, ok2 := conn.Streams[2] if !ok2 { t.Fatal("stream 2 devrait exister") } if stream2.Initiator != 1 { t.Errorf("stream 2 Initiator: attendu 1 (serveur), obtenu %d", stream2.Initiator) } } // TestH2ConnStateStreamStateMachine verifies open → half-closed → closed transitions. func TestH2ConnStateStreamStateMachine(t *testing.T) { conn := parser.NewH2ConnState() // Stream 1: HEADERS with END_STREAM (client sends request + END_STREAM) h1 := []byte{0x82, 0x84} // :method GET, :path / frame1 := buildH2Frame(0x1, 0x05, 1, h1) // HEADERS, END_STREAM + END_HEADERS conn.ProcessFrames(frame1, 0) stream1, ok := conn.Streams[1] if !ok { t.Fatal("stream 1 devrait exister") } if stream1.State != "half-closed-remote" { t.Errorf("après END_STREAM client: état attendu 'half-closed-remote', obtenu %q", stream1.State) } // Server responds with END_STREAM → closed h2 := []byte{0x88} // :status 200 frame2 := buildH2Frame(0x1, 0x05, 1, h2) // HEADERS, END_STREAM + END_HEADERS conn.ProcessFrames(frame2, 1) if stream1.State != "closed" { t.Errorf("après END_STREAM serveur: état attendu 'closed', obtenu %q", stream1.State) } } // TestH2ConnStateStreamFrameHistory verifies FrameTypes accumulation per stream. func TestH2ConnStateStreamFrameHistory(t *testing.T) { conn := parser.NewH2ConnState() // HEADERS on stream 1 h1 := []byte{0x82, 0x84} frame1 := buildH2Frame(0x1, 0x04, 1, h1) // HEADERS, END_HEADERS conn.ProcessFrames(frame1, 0) // DATA on stream 1 dataPayload := []byte("hello") dataFrame := buildH2Frame(0x0, 0x01, 1, dataPayload) // DATA, END_STREAM conn.ProcessFrames(dataFrame, 0) stream, ok := conn.Streams[1] if !ok { t.Fatal("stream 1 devrait exister") } if len(stream.FrameTypes) != 2 { t.Fatalf("FrameTypes: attendu 2, obtenu %d", len(stream.FrameTypes)) } if stream.FrameTypes[0] != http2.FrameHeaders { t.Errorf("FrameTypes[0]: attendu HEADERS, obtenu %v", stream.FrameTypes[0]) } if stream.FrameTypes[1] != http2.FrameData { t.Errorf("FrameTypes[1]: attendu DATA, obtenu %v", stream.FrameTypes[1]) } } // TestH2ConnStateMultipleFramesInBatch verifies frame index persistence across calls. func TestH2ConnStateMultipleFramesInBatch(t *testing.T) { conn := parser.NewH2ConnState() // First call: SETTINGS + HEADERS settingsPayload := []byte{ 0x00, 0x01, 0x00, 0x00, 0x10, 0x00, // HEADER_TABLE_SIZE = 4096 } settingsFrame := buildH2Frame(0x4, 0x0, 0, settingsPayload) h1 := []byte{0x82, 0x84} headersFrame := buildH2Frame(0x1, 0x04, 1, h1) data1 := append(settingsFrame, headersFrame...) result1, _ := conn.ProcessFrames(data1, 0) if len(result1.Frames) != 2 { t.Fatalf("Batch 1: attendu 2 frames, obtenu %d", len(result1.Frames)) } if result1.Frames[0].Index != 1 || result1.Frames[1].Index != 2 { t.Errorf("Batch 1 indices: attendu [1,2], obtenu [%d,%d]", result1.Frames[0].Index, result1.Frames[1].Index) } // Second call: PING → index should continue at 3 pingPayload := make([]byte, 8) pingFrame := buildH2Frame(0x6, 0x0, 0, pingPayload) result2, _ := conn.ProcessFrames(pingFrame, 0) if len(result2.Frames) != 1 { t.Fatalf("Batch 2: attendu 1 frame, obtenu %d", len(result2.Frames)) } if result2.Frames[0].Index != 3 { t.Errorf("Batch 2 index: attendu 3, obtenu %d", result2.Frames[0].Index) } }