Skip to content

Commit 9cfe9c9

Browse files
authored
test(bbr): add unit tests for body-field-to-header plugin (kubernetes-sigs#2569)
Add comprehensive unit tests for BodyFieldToHeaderPlugin covering: - Constructor validation (NewBodyFieldToHeaderPlugin) - Factory config parsing (BodyFieldToHeaderPluginFactory) - Request processing with various value types - Error paths (missing field, empty field, nil inputs) - Header mutation tracking via MutatedHeaders() 25 test cases achieving 100% statement coverage. Signed-off-by: Asaad Balum <asaad.balum@gmail.com> Signed-off-by: asaadbalum <asaad.balum@gmail.com>
1 parent e7a78e4 commit 9cfe9c9

File tree

1 file changed

+335
-0
lines changed

1 file changed

+335
-0
lines changed
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
/*
2+
Copyright 2026 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package plugins
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"testing"
23+
24+
"sigs.k8s.io/gateway-api-inference-extension/pkg/bbr/framework"
25+
)
26+
27+
const testModelValue = "llama-3"
28+
29+
func TestNewBodyFieldToHeaderPlugin(t *testing.T) {
30+
tests := []struct {
31+
name string
32+
fieldName string
33+
headerName string
34+
wantErr bool
35+
}{
36+
{
37+
name: "valid config",
38+
fieldName: "model",
39+
headerName: "X-Gateway-Model",
40+
},
41+
{
42+
name: "empty field name",
43+
fieldName: "",
44+
headerName: "X-Gateway-Model",
45+
wantErr: true,
46+
},
47+
{
48+
name: "empty header name",
49+
fieldName: "model",
50+
headerName: "",
51+
wantErr: true,
52+
},
53+
{
54+
name: "both empty",
55+
fieldName: "",
56+
headerName: "",
57+
wantErr: true,
58+
},
59+
}
60+
for _, tt := range tests {
61+
t.Run(tt.name, func(t *testing.T) {
62+
p, err := NewBodyFieldToHeaderPlugin(tt.fieldName, tt.headerName)
63+
if tt.wantErr {
64+
if err == nil {
65+
t.Fatal("expected error, got nil")
66+
}
67+
return
68+
}
69+
if err != nil {
70+
t.Fatalf("unexpected error: %v", err)
71+
}
72+
if p.TypedName().Type != BodyFieldToHeaderPluginType {
73+
t.Errorf("Type = %q, want %q", p.TypedName().Type, BodyFieldToHeaderPluginType)
74+
}
75+
if p.TypedName().Name != BodyFieldToHeaderPluginType {
76+
t.Errorf("Name = %q, want %q", p.TypedName().Name, BodyFieldToHeaderPluginType)
77+
}
78+
})
79+
}
80+
}
81+
82+
func TestBodyFieldToHeaderPlugin_WithName(t *testing.T) {
83+
p, err := NewBodyFieldToHeaderPlugin("model", "X-Gateway-Model")
84+
if err != nil {
85+
t.Fatalf("unexpected error: %v", err)
86+
}
87+
88+
p.WithName("custom-name")
89+
90+
if got := p.TypedName().Name; got != "custom-name" {
91+
t.Errorf("Name after WithName = %q, want %q", got, "custom-name")
92+
}
93+
if got := p.TypedName().Type; got != BodyFieldToHeaderPluginType {
94+
t.Errorf("Type should be unchanged = %q, want %q", got, BodyFieldToHeaderPluginType)
95+
}
96+
}
97+
98+
func TestBodyFieldToHeaderPluginFactory(t *testing.T) {
99+
tests := []struct {
100+
name string
101+
pluginName string
102+
rawParams json.RawMessage
103+
wantErr bool
104+
wantName string
105+
}{
106+
{
107+
name: "valid config",
108+
pluginName: "my-plugin",
109+
rawParams: json.RawMessage(`{"field_name":"model","header_name":"X-Gateway-Model"}`),
110+
wantName: "my-plugin",
111+
},
112+
{
113+
name: "invalid JSON",
114+
pluginName: "my-plugin",
115+
rawParams: json.RawMessage(`{invalid`),
116+
wantErr: true,
117+
},
118+
{
119+
name: "missing field_name",
120+
pluginName: "my-plugin",
121+
rawParams: json.RawMessage(`{"header_name":"X-Gateway-Model"}`),
122+
wantErr: true,
123+
},
124+
{
125+
name: "missing header_name",
126+
pluginName: "my-plugin",
127+
rawParams: json.RawMessage(`{"field_name":"model"}`),
128+
wantErr: true,
129+
},
130+
{
131+
name: "empty parameters",
132+
pluginName: "my-plugin",
133+
rawParams: json.RawMessage(``),
134+
wantErr: true,
135+
},
136+
{
137+
name: "null parameters",
138+
pluginName: "my-plugin",
139+
rawParams: nil,
140+
wantErr: true,
141+
},
142+
{
143+
name: "JSON null",
144+
pluginName: "my-plugin",
145+
rawParams: json.RawMessage(`null`),
146+
wantErr: true,
147+
},
148+
{
149+
name: "empty JSON object",
150+
pluginName: "my-plugin",
151+
rawParams: json.RawMessage(`{}`),
152+
wantErr: true,
153+
},
154+
}
155+
for _, tt := range tests {
156+
t.Run(tt.name, func(t *testing.T) {
157+
p, err := BodyFieldToHeaderPluginFactory(tt.pluginName, tt.rawParams)
158+
if tt.wantErr {
159+
if err == nil {
160+
t.Fatal("expected error, got nil")
161+
}
162+
return
163+
}
164+
if err != nil {
165+
t.Fatalf("unexpected error: %v", err)
166+
}
167+
if got := p.TypedName().Name; got != tt.wantName {
168+
t.Errorf("Name = %q, want %q", got, tt.wantName)
169+
}
170+
if got := p.TypedName().Type; got != BodyFieldToHeaderPluginType {
171+
t.Errorf("Type = %q, want %q", got, BodyFieldToHeaderPluginType)
172+
}
173+
})
174+
}
175+
}
176+
177+
func TestBodyFieldToHeaderPlugin_ProcessRequest(t *testing.T) {
178+
tests := []struct {
179+
name string
180+
fieldName string
181+
headerName string
182+
request *framework.InferenceRequest
183+
wantErr bool
184+
wantHeader string
185+
}{
186+
{
187+
name: "string field value",
188+
fieldName: "model",
189+
headerName: "X-Gateway-Model",
190+
request: func() *framework.InferenceRequest {
191+
r := framework.NewInferenceRequest()
192+
r.Body["model"] = testModelValue
193+
return r
194+
}(),
195+
wantHeader: testModelValue,
196+
},
197+
{
198+
name: "integer field value",
199+
fieldName: "count",
200+
headerName: "X-Gateway-Count",
201+
request: func() *framework.InferenceRequest {
202+
r := framework.NewInferenceRequest()
203+
r.Body["count"] = 42
204+
return r
205+
}(),
206+
wantHeader: "42",
207+
},
208+
{
209+
name: "float field value",
210+
fieldName: "temperature",
211+
headerName: "X-Gateway-Temp",
212+
request: func() *framework.InferenceRequest {
213+
r := framework.NewInferenceRequest()
214+
r.Body["temperature"] = 0.7
215+
return r
216+
}(),
217+
wantHeader: "0.7",
218+
},
219+
{
220+
name: "boolean field value",
221+
fieldName: "stream",
222+
headerName: "X-Gateway-Stream",
223+
request: func() *framework.InferenceRequest {
224+
r := framework.NewInferenceRequest()
225+
r.Body["stream"] = true
226+
return r
227+
}(),
228+
wantHeader: "true",
229+
},
230+
{
231+
name: "field not found",
232+
fieldName: "missing",
233+
headerName: "X-Gateway-Missing",
234+
request: func() *framework.InferenceRequest {
235+
r := framework.NewInferenceRequest()
236+
r.Body["other"] = "value"
237+
return r
238+
}(),
239+
wantErr: true,
240+
},
241+
{
242+
name: "field is empty string",
243+
fieldName: "model",
244+
headerName: "X-Gateway-Model",
245+
request: func() *framework.InferenceRequest {
246+
r := framework.NewInferenceRequest()
247+
r.Body["model"] = ""
248+
return r
249+
}(),
250+
wantErr: true,
251+
},
252+
{
253+
name: "nil field value",
254+
fieldName: "model",
255+
headerName: "X-Gateway-Model",
256+
request: func() *framework.InferenceRequest {
257+
r := framework.NewInferenceRequest()
258+
r.Body["model"] = nil
259+
return r
260+
}(),
261+
wantHeader: "<nil>",
262+
},
263+
{
264+
name: "nil request",
265+
fieldName: "model",
266+
headerName: "X-Gateway-Model",
267+
request: nil,
268+
},
269+
{
270+
name: "nil headers",
271+
fieldName: "model",
272+
headerName: "X-Gateway-Model",
273+
request: &framework.InferenceRequest{
274+
InferenceMessage: framework.InferenceMessage{
275+
Headers: nil,
276+
Body: map[string]any{"model": "llama"},
277+
},
278+
},
279+
},
280+
{
281+
name: "nil body",
282+
fieldName: "model",
283+
headerName: "X-Gateway-Model",
284+
request: &framework.InferenceRequest{
285+
InferenceMessage: framework.InferenceMessage{
286+
Headers: map[string]string{},
287+
Body: nil,
288+
},
289+
},
290+
},
291+
}
292+
for _, tt := range tests {
293+
t.Run(tt.name, func(t *testing.T) {
294+
p, err := NewBodyFieldToHeaderPlugin(tt.fieldName, tt.headerName)
295+
if err != nil {
296+
t.Fatalf("failed to create plugin: %v", err)
297+
}
298+
299+
err = p.ProcessRequest(context.Background(), tt.request)
300+
if tt.wantErr {
301+
if err == nil {
302+
t.Fatal("expected error, got nil")
303+
}
304+
return
305+
}
306+
if err != nil {
307+
t.Fatalf("unexpected error: %v", err)
308+
}
309+
if tt.wantHeader != "" {
310+
if got := tt.request.Headers[tt.headerName]; got != tt.wantHeader {
311+
t.Errorf("Headers[%q] = %q, want %q", tt.headerName, got, tt.wantHeader)
312+
}
313+
}
314+
})
315+
}
316+
}
317+
318+
func TestBodyFieldToHeaderPlugin_ProcessRequest_MutatedHeaders(t *testing.T) {
319+
p, err := NewBodyFieldToHeaderPlugin("model", "X-Gateway-Model")
320+
if err != nil {
321+
t.Fatalf("failed to create plugin: %v", err)
322+
}
323+
324+
request := framework.NewInferenceRequest()
325+
request.Body["model"] = testModelValue
326+
327+
if err := p.ProcessRequest(context.Background(), request); err != nil {
328+
t.Fatalf("unexpected error: %v", err)
329+
}
330+
331+
mutated := request.MutatedHeaders()
332+
if got, ok := mutated["X-Gateway-Model"]; !ok || got != testModelValue {
333+
t.Errorf("MutatedHeaders[\"X-Gateway-Model\"] = %q, %v; want %q, true", got, ok, testModelValue)
334+
}
335+
}

0 commit comments

Comments
 (0)