k8s.io/apiserver@v0.31.1/pkg/endpoints/handlers/negotiation/negotiate_test.go (about)

     1  /*
     2  Copyright 2015 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 negotiation
    18  
    19  import (
    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
    31  type statusError interface {
    32  	Status() metav1.Status
    33  }
    34  
    35  type fakeNegotiater struct {
    36  	serializer, streamSerializer runtime.Serializer
    37  	framer                       runtime.Framer
    38  	types, streamTypes           []string
    39  }
    40  
    41  func (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  
    75  func (n *fakeNegotiater) EncoderForVersion(serializer runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder {
    76  	return n.serializer
    77  }
    78  
    79  func (n *fakeNegotiater) DecoderToVersion(serializer runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
    80  	return n.serializer
    81  }
    82  
    83  var fakeCodec = runtime.NewCodec(runtime.NoopEncoder{}, runtime.NoopDecoder{})
    84  
    85  func 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  
   284  func 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  
   299  func 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  }