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