1/*
2Copyright 2015 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package negotiation
18
19import (
20	"mime"
21	"net/http"
22	"net/url"
23	"strings"
24	"testing"
25
26	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27	"k8s.io/apimachinery/pkg/runtime"
28)
29
30// statusError is an object that can be converted into an metav1.Status
31type statusError interface {
32	Status() metav1.Status
33}
34
35type fakeNegotiater struct {
36	serializer, streamSerializer runtime.Serializer
37	framer                       runtime.Framer
38	types, streamTypes           []string
39}
40
41func (n *fakeNegotiater) SupportedMediaTypes() []runtime.SerializerInfo {
42	var out []runtime.SerializerInfo
43	for _, s := range n.types {
44		mediaType, _, err := mime.ParseMediaType(s)
45		if err != nil {
46			panic(err)
47		}
48		parts := strings.SplitN(mediaType, "/", 2)
49		if len(parts) == 1 {
50			// this is an error on the server side
51			parts = append(parts, "")
52		}
53
54		info := runtime.SerializerInfo{
55			Serializer:       n.serializer,
56			MediaType:        s,
57			MediaTypeType:    parts[0],
58			MediaTypeSubType: parts[1],
59			EncodesAsText:    true,
60		}
61		for _, t := range n.streamTypes {
62			if t == s {
63				info.StreamSerializer = &runtime.StreamSerializerInfo{
64					EncodesAsText: true,
65					Framer:        n.framer,
66					Serializer:    n.streamSerializer,
67				}
68			}
69		}
70		out = append(out, info)
71	}
72	return out
73}
74
75func (n *fakeNegotiater) EncoderForVersion(serializer runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder {
76	return n.serializer
77}
78
79func (n *fakeNegotiater) DecoderToVersion(serializer runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
80	return n.serializer
81}
82
83var fakeCodec = runtime.NewCodec(runtime.NoopEncoder{}, runtime.NoopDecoder{})
84
85func TestNegotiate(t *testing.T) {
86	testCases := []struct {
87		accept      string
88		req         *http.Request
89		ns          *fakeNegotiater
90		serializer  runtime.Serializer
91		contentType string
92		params      map[string]string
93		errFn       func(error) bool
94	}{
95		// pick a default
96		{
97			req:         &http.Request{},
98			contentType: "application/json",
99			ns:          &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
100			serializer:  fakeCodec,
101		},
102		{
103			accept:      "",
104			contentType: "application/json",
105			ns:          &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
106			serializer:  fakeCodec,
107		},
108		{
109			accept:      "*/*",
110			contentType: "application/json",
111			ns:          &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
112			serializer:  fakeCodec,
113		},
114		{
115			accept:      "application/*",
116			contentType: "application/json",
117			ns:          &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
118			serializer:  fakeCodec,
119		},
120		{
121			accept:      "application/json",
122			contentType: "application/json",
123			ns:          &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
124			serializer:  fakeCodec,
125		},
126		{
127			accept:      "application/json",
128			contentType: "application/json",
129			ns:          &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json", "application/protobuf"}},
130			serializer:  fakeCodec,
131		},
132		{
133			accept:      "application/protobuf",
134			contentType: "application/protobuf",
135			ns:          &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json", "application/protobuf"}},
136			serializer:  fakeCodec,
137		},
138		{
139			accept:      "application/json; pretty=1",
140			contentType: "application/json",
141			ns:          &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
142			serializer:  fakeCodec,
143			params:      map[string]string{"pretty": "1"},
144		},
145		{
146			accept:      "unrecognized/stuff,application/json; pretty=1",
147			contentType: "application/json",
148			ns:          &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
149			serializer:  fakeCodec,
150			params:      map[string]string{"pretty": "1"},
151		},
152
153		// query param triggers pretty
154		{
155			req: &http.Request{
156				Header: http.Header{"Accept": []string{"application/json"}},
157				URL:    &url.URL{RawQuery: "pretty=1"},
158			},
159			contentType: "application/json",
160			ns:          &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
161			serializer:  fakeCodec,
162			params:      map[string]string{"pretty": "1"},
163		},
164
165		// certain user agents trigger pretty
166		{
167			req: &http.Request{
168				Header: http.Header{
169					"Accept":     []string{"application/json"},
170					"User-Agent": []string{"curl"},
171				},
172			},
173			contentType: "application/json",
174			ns:          &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
175			serializer:  fakeCodec,
176			params:      map[string]string{"pretty": "1"},
177		},
178		{
179			req: &http.Request{
180				Header: http.Header{
181					"Accept":     []string{"application/json"},
182					"User-Agent": []string{"Wget"},
183				},
184			},
185			contentType: "application/json",
186			ns:          &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
187			serializer:  fakeCodec,
188			params:      map[string]string{"pretty": "1"},
189		},
190		{
191			req: &http.Request{
192				Header: http.Header{
193					"Accept":     []string{"application/json"},
194					"User-Agent": []string{"Mozilla/5.0"},
195				},
196			},
197			contentType: "application/json",
198			ns:          &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
199			serializer:  fakeCodec,
200			params:      map[string]string{"pretty": "1"},
201		},
202		{
203			req: &http.Request{
204				Header: http.Header{
205					"Accept": []string{"application/json;as=BOGUS;v=v1beta1;g=meta.k8s.io, application/json"},
206				},
207			},
208			contentType: "application/json",
209			ns:          &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
210			serializer:  fakeCodec,
211		},
212		{
213			req: &http.Request{
214				Header: http.Header{
215					"Accept": []string{"application/BOGUS, application/json"},
216				},
217			},
218			contentType: "application/json",
219			ns:          &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
220			serializer:  fakeCodec,
221		},
222		// "application" is not a valid media type, so the server will reject the response during
223		// negotiation (the server, in error, has specified an invalid media type)
224		{
225			accept: "application",
226			ns:     &fakeNegotiater{serializer: fakeCodec, types: []string{"application"}},
227			errFn: func(err error) bool {
228				return err.Error() == "only the following media types are accepted: application"
229			},
230		},
231		{
232			ns: &fakeNegotiater{},
233			errFn: func(err error) bool {
234				return err.Error() == "only the following media types are accepted: "
235			},
236		},
237		{
238			accept: "*/*",
239			ns:     &fakeNegotiater{},
240			errFn: func(err error) bool {
241				return err.Error() == "only the following media types are accepted: "
242			},
243		},
244	}
245
246	for i, test := range testCases {
247		req := test.req
248		if req == nil {
249			req = &http.Request{Header: http.Header{}}
250			req.Header.Set("Accept", test.accept)
251		}
252		_, s, err := NegotiateOutputMediaType(req, test.ns, DefaultEndpointRestrictions)
253		switch {
254		case err == nil && test.errFn != nil:
255			t.Errorf("%d: failed: expected error", i)
256			continue
257		case err != nil && test.errFn == nil:
258			t.Errorf("%d: failed: %v", i, err)
259			continue
260		case err != nil:
261			if !test.errFn(err) {
262				t.Errorf("%d: failed: %v", i, err)
263			}
264			status, ok := err.(statusError)
265			if !ok {
266				t.Errorf("%d: failed, error should be statusError: %v", i, err)
267				continue
268			}
269			if status.Status().Status != metav1.StatusFailure || status.Status().Code != http.StatusNotAcceptable {
270				t.Errorf("%d: failed: %v", i, err)
271				continue
272			}
273			continue
274		}
275		if test.contentType != s.MediaType {
276			t.Errorf("%d: unexpected %s %s", i, test.contentType, s.MediaType)
277		}
278		if s.Serializer != test.serializer {
279			t.Errorf("%d: unexpected %s %s", i, test.serializer, s.Serializer)
280		}
281	}
282}
283
284func fakeSerializerInfoSlice() []runtime.SerializerInfo {
285	result := make([]runtime.SerializerInfo, 2)
286	result[0] = runtime.SerializerInfo{
287		MediaType:        "application/json",
288		MediaTypeType:    "application",
289		MediaTypeSubType: "json",
290	}
291	result[1] = runtime.SerializerInfo{
292		MediaType:        "application/vnd.kubernetes.protobuf",
293		MediaTypeType:    "application",
294		MediaTypeSubType: "vnd.kubernetes.protobuf",
295	}
296	return result
297}
298
299func BenchmarkNegotiateMediaTypeOptions(b *testing.B) {
300	accepted := fakeSerializerInfoSlice()
301	header := "application/vnd.kubernetes.protobuf,*/*"
302
303	for i := 0; i < b.N; i++ {
304		options, _ := NegotiateMediaTypeOptions(header, accepted, DefaultEndpointRestrictions)
305		if options.Accepted != accepted[1] {
306			b.Errorf("Unexpected result")
307		}
308	}
309}
310