package helpers_test import ( "bytes" "testing" "github.com/jfraeys/fetch_ml/internal/api/helpers" ) func TestNewPayloadParser(t *testing.T) { payload := []byte{0x01, 0x02, 0x03, 0x04, 0x05} p := helpers.NewPayloadParser(payload, 2) if p.Offset() != 2 { t.Errorf("expected offset 2, got %d", p.Offset()) } if !bytes.Equal(p.Payload(), payload) { t.Error("payload mismatch") } } func TestPayloadParser_ParseByte(t *testing.T) { tests := []struct { name string payload []byte start int want byte wantErr bool }{ { name: "valid byte", payload: []byte{0x00, 0x01, 0x02}, start: 1, want: 0x01, wantErr: false, }, { name: "end of payload", payload: []byte{0x00, 0x01}, start: 2, want: 0, wantErr: true, }, { name: "empty payload", payload: []byte{}, start: 0, want: 0, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := helpers.NewPayloadParser(tt.payload, tt.start) got, err := p.ParseByte() if (err != nil) != tt.wantErr { t.Errorf("ParseByte() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("ParseByte() = %v, want %v", got, tt.want) } }) } } func TestPayloadParser_ParseUint16(t *testing.T) { tests := []struct { name string payload []byte start int want uint16 wantErr bool }{ { name: "valid uint16", payload: []byte{0x01, 0x02, 0x03, 0x04}, start: 0, want: 0x0102, wantErr: false, }, { name: "another valid uint16", payload: []byte{0x00, 0xAB, 0xCD}, start: 1, want: 0xABCD, wantErr: false, }, { name: "too short", payload: []byte{0x00, 0x01}, start: 1, want: 0, wantErr: true, }, { name: "empty at end", payload: []byte{0x00, 0x01, 0x02}, start: 3, want: 0, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := helpers.NewPayloadParser(tt.payload, tt.start) got, err := p.ParseUint16() if (err != nil) != tt.wantErr { t.Errorf("ParseUint16() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("ParseUint16() = %v, want %v", got, tt.want) } }) } } func TestPayloadParser_ParseLengthPrefixedString(t *testing.T) { tests := []struct { name string payload []byte start int want string wantErr bool }{ { name: "valid string", payload: []byte{0x05, 'h', 'e', 'l', 'l', 'o'}, start: 0, want: "hello", wantErr: false, }, { name: "empty string", payload: []byte{0x00, 0x01, 0x02}, start: 0, want: "", wantErr: false, }, { name: "from offset", payload: []byte{0x00, 0x03, 'f', 'o', 'o'}, start: 1, want: "foo", wantErr: false, }, { name: "too short for length", payload: []byte{0x05, 'h', 'i'}, start: 0, want: "", wantErr: true, }, { name: "no length byte", payload: []byte{}, start: 0, want: "", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := helpers.NewPayloadParser(tt.payload, tt.start) got, err := p.ParseLengthPrefixedString() if (err != nil) != tt.wantErr { t.Errorf("ParseLengthPrefixedString() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("ParseLengthPrefixedString() = %v, want %v", got, tt.want) } }) } } func TestPayloadParser_ParseUint16PrefixedString(t *testing.T) { tests := []struct { name string payload []byte start int want string wantErr bool }{ { name: "valid string", payload: []byte{0x00, 0x05, 'h', 'e', 'l', 'l', 'o'}, start: 0, want: "hello", wantErr: false, }, { name: "empty string", payload: []byte{0x00, 0x00, 0x01, 0x02}, start: 0, want: "", wantErr: false, }, { name: "from offset", payload: []byte{0x00, 0x00, 0x00, 0x03, 'b', 'a', 'r'}, start: 2, want: "bar", wantErr: false, }, { name: "too short for length", payload: []byte{0x00}, start: 0, want: "", wantErr: true, }, { name: "too short for string", payload: []byte{0x00, 0x05, 'h', 'i'}, start: 0, want: "", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := helpers.NewPayloadParser(tt.payload, tt.start) got, err := p.ParseUint16PrefixedString() if (err != nil) != tt.wantErr { t.Errorf("ParseUint16PrefixedString() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("ParseUint16PrefixedString() = %v, want %v", got, tt.want) } }) } } func TestPayloadParser_ParseBool(t *testing.T) { tests := []struct { name string payload []byte start int want bool wantErr bool }{ { name: "true", payload: []byte{0x01, 0x00}, start: 0, want: true, wantErr: false, }, { name: "false", payload: []byte{0x00, 0x01}, start: 0, want: false, wantErr: false, }, { name: "non-zero is true", payload: []byte{0xFF}, start: 0, want: true, wantErr: false, }, { name: "empty", payload: []byte{}, start: 0, want: false, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := helpers.NewPayloadParser(tt.payload, tt.start) got, err := p.ParseBool() if (err != nil) != tt.wantErr { t.Errorf("ParseBool() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("ParseBool() = %v, want %v", got, tt.want) } }) } } func TestPayloadParser_ParseFixedBytes(t *testing.T) { tests := []struct { name string payload []byte start int length int want []byte wantErr bool }{ { name: "valid", payload: []byte{0x00, 0x01, 0x02, 0x03, 0x04}, start: 1, length: 3, want: []byte{0x01, 0x02, 0x03}, wantErr: false, }, { name: "exact length", payload: []byte{0x00, 0x01, 0x02}, start: 0, length: 3, want: []byte{0x00, 0x01, 0x02}, wantErr: false, }, { name: "too long", payload: []byte{0x00, 0x01}, start: 0, length: 3, want: nil, wantErr: true, }, { name: "from end", payload: []byte{0x00, 0x01, 0x02}, start: 2, length: 2, want: nil, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := helpers.NewPayloadParser(tt.payload, tt.start) got, err := p.ParseFixedBytes(tt.length) if (err != nil) != tt.wantErr { t.Errorf("ParseFixedBytes() error = %v, wantErr %v", err, tt.wantErr) return } if !bytes.Equal(got, tt.want) { t.Errorf("ParseFixedBytes() = %v, want %v", got, tt.want) } }) } } func TestPayloadParser_Offset(t *testing.T) { payload := []byte{0x00, 0x01, 0x02, 0x03} p := helpers.NewPayloadParser(payload, 1) if p.Offset() != 1 { t.Errorf("initial Offset() = %d, want 1", p.Offset()) } p.ParseByte() if p.Offset() != 2 { t.Errorf("after ParseByte() Offset() = %d, want 2", p.Offset()) } } func TestPayloadParser_Remaining(t *testing.T) { payload := []byte{0x00, 0x01, 0x02, 0x03} p := helpers.NewPayloadParser(payload, 2) remaining := p.Remaining() if !bytes.Equal(remaining, []byte{0x02, 0x03}) { t.Errorf("Remaining() = %v, want [0x02, 0x03]", remaining) } // At end p = helpers.NewPayloadParser(payload, 4) if p.Remaining() != nil { t.Errorf("at end Remaining() = %v, want nil", p.Remaining()) } // Beyond end p = helpers.NewPayloadParser(payload, 5) if p.Remaining() != nil { t.Errorf("beyond end Remaining() = %v, want nil", p.Remaining()) } } func TestPayloadParser_HasRemaining(t *testing.T) { payload := []byte{0x00, 0x01} tests := []struct { start int want bool }{ {0, true}, {1, true}, {2, false}, {3, false}, } for _, tt := range tests { p := helpers.NewPayloadParser(payload, tt.start) if got := p.HasRemaining(); got != tt.want { t.Errorf("HasRemaining() with start=%d = %v, want %v", tt.start, got, tt.want) } } } func TestPayloadParser_ChainedParsing(t *testing.T) { // Simulate a real protocol: [api_key:2][name_len:1][name:var][value:2] payload := []byte{ 0xAB, 0xCD, // api_key 0x05, // name_len 'h', 'e', 'l', 'l', 'o', // name 0x00, 0x42, // value } p := helpers.NewPayloadParser(payload, 0) // Parse api key (2 bytes) apiKey, err := p.ParseFixedBytes(2) if err != nil { t.Fatalf("failed to parse api key: %v", err) } if !bytes.Equal(apiKey, []byte{0xAB, 0xCD}) { t.Errorf("api key = %v, want [0xAB, 0xCD]", apiKey) } // Parse name name, err := p.ParseLengthPrefixedString() if err != nil { t.Fatalf("failed to parse name: %v", err) } if name != "hello" { t.Errorf("name = %q, want 'hello'", name) } // Parse value value, err := p.ParseUint16() if err != nil { t.Fatalf("failed to parse value: %v", err) } if value != 0x0042 { t.Errorf("value = 0x%X, want 0x0042", value) } // Should be at end if p.HasRemaining() { t.Error("expected no remaining bytes") } }