package capture import ( "encoding/binary" "os" "testing" "time" "github.com/gopacket/gopacket/pcapgo" "github.com/gopacket/gopacket" ) // createTestPcap writes a pcap file with the given raw packet bytes and returns the temp file path. // Packets are written with Ethernet link type (1). func createTestPcap(t *testing.T, packets [][]byte) string { t.Helper() f, err := os.CreateTemp("", "test-*.pcap") if err != nil { t.Fatalf("createTestPcap: os.CreateTemp: %v", err) } defer f.Close() w := pcapgo.NewWriter(f) // Write global header: Ethernet link type (1) if err := w.WriteFileHeader(65535, 1); err != nil { t.Fatalf("createTestPcap: WriteFileHeader: %v", err) } ts := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) for _, pkt := range packets { ci := gopacket.CaptureInfo{ Timestamp: ts, CaptureLength: len(pkt), Length: len(pkt), } if err := w.WritePacket(ci, pkt); err != nil { t.Fatalf("createTestPcap: WritePacket: %v", err) } ts = ts.Add(100 * time.Millisecond) } return f.Name() } // minimalEthernetIPTCPPacket builds a minimal raw Ethernet+IPv4+TCP packet. // dstPort is the TCP destination port. Used for filter tests. func minimalEthernetIPTCPPacket(dstPort uint16) []byte { pkt := make([]byte, 54) // Ethernet header (14 bytes): dst MAC, src MAC, EtherType=IPv4(0x0800) pkt[12] = 0x08 pkt[13] = 0x00 // IPv4 header (20 bytes) starting at offset 14 pkt[14] = 0x45 // version=4, IHL=5 pkt[23] = 0x06 // protocol = TCP (6) pkt[16] = 0x00 // total length high pkt[17] = 0x28 // total length low (40 = 20 IP + 20 TCP) // TCP header (20 bytes) starting at offset 34 binary.BigEndian.PutUint16(pkt[36:38], dstPort) // dst port return pkt } // minimalEthernetIPUDPPacket builds a minimal raw Ethernet+IPv4+UDP packet. func minimalEthernetIPUDPPacket(dstPort uint16) []byte { pkt := make([]byte, 42) // Ethernet header (14 bytes) pkt[12] = 0x08 pkt[13] = 0x00 // IPv4 header (20 bytes) starting at offset 14 pkt[14] = 0x45 // version=4, IHL=5 pkt[23] = 0x11 // protocol = UDP (17) pkt[16] = 0x00 pkt[17] = 0x1C // total length (28 = 20 IP + 8 UDP) // UDP header (8 bytes) starting at offset 34 binary.BigEndian.PutUint16(pkt[36:38], dstPort) // dst port return pkt } // TestReadPcapFile verifies that ReadPcapFile returns exactly 3 packets and closes. func TestReadPcapFile(t *testing.T) { pkts := [][]byte{ minimalEthernetIPTCPPacket(80), minimalEthernetIPTCPPacket(443), minimalEthernetIPUDPPacket(53), } path := createTestPcap(t, pkts) defer os.Remove(path) ch, err := ReadPcapFile(path, "") if err != nil { t.Fatalf("ReadPcapFile: unexpected error: %v", err) } var count int for range ch { count++ } if count != 3 { t.Errorf("ReadPcapFile: got %d packets; want 3", count) } } // TestReadPcapFileNotFound verifies error when file does not exist. func TestReadPcapFileNotFound(t *testing.T) { _, err := ReadPcapFile("/nonexistent/file.pcap", "") if err == nil { t.Fatal("ReadPcapFile(\"/nonexistent/file.pcap\"): expected error, got nil") } errStr := err.Error() if len(errStr) == 0 { t.Error("ReadPcapFile: error message is empty") } // Should contain "cannot open" if !contains(errStr, "cannot open") { t.Errorf("ReadPcapFile error %q does not contain 'cannot open'", errStr) } } // TestReadPcapFileInvalid verifies error when file contains garbage bytes (not a pcap). func TestReadPcapFileInvalid(t *testing.T) { f, err := os.CreateTemp("", "invalid-*.pcap") if err != nil { t.Fatalf("os.CreateTemp: %v", err) } defer os.Remove(f.Name()) f.Write([]byte("this is not a pcap file at all, just garbage bytes 12345")) f.Close() _, err = ReadPcapFile(f.Name(), "") if err == nil { t.Fatal("ReadPcapFile on garbage file: expected error, got nil") } } // TestReadPcapFileWithFilter verifies that filter="tcp" only passes TCP packets. func TestReadPcapFileWithFilter(t *testing.T) { pkts := [][]byte{ minimalEthernetIPTCPPacket(80), // TCP minimalEthernetIPUDPPacket(53), // UDP minimalEthernetIPTCPPacket(443), // TCP } path := createTestPcap(t, pkts) defer os.Remove(path) ch, err := ReadPcapFile(path, "tcp") if err != nil { t.Fatalf("ReadPcapFile with filter: unexpected error: %v", err) } var count int for range ch { count++ } // Only TCP packets should pass if count != 2 { t.Errorf("ReadPcapFile with filter=tcp: got %d packets; want 2 (TCP only)", count) } } // contains is a simple string containment check. func contains(s, substr string) bool { return len(s) >= len(substr) && (s == substr || func() bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false }()) } // TestClassifiedPacketTimestamp verifies that ClassifiedPacket has a Timestamp field of type time.Time. // This test verifies the struct shape at compile time. func TestClassifiedPacketTimestamp(t *testing.T) { // This test compiles only if ClassifiedPacket has a Timestamp time.Time field. _ = time.Time{} // The import path is in classify, but the test is in capture. // We verify indirectly via ReadPcapFile returning packets with metadata. // The actual struct test is in classify package. }