github.com/useflyent/fhttp@v0.0.0-20211004035111-333f430cfbbf/header_test.go (about) 1 // Copyright 2011 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package http 6 7 import ( 8 "bytes" 9 "reflect" 10 "runtime" 11 "testing" 12 "time" 13 14 "github.com/useflyent/fhttp/internal/race" 15 ) 16 17 var headerWriteTests = []struct { 18 h Header 19 exclude map[string]bool 20 expected string 21 }{ 22 {Header{}, nil, ""}, 23 { 24 Header{ 25 "Content-Type": {"text/html; charset=UTF-8"}, 26 "Content-Length": {"0"}, 27 }, 28 nil, 29 "Content-Length: 0\r\nContent-Type: text/html; charset=UTF-8\r\n", 30 }, 31 { 32 Header{ 33 "Content-Length": {"0", "1", "2"}, 34 }, 35 nil, 36 "Content-Length: 0\r\nContent-Length: 1\r\nContent-Length: 2\r\n", 37 }, 38 { 39 Header{ 40 "Expires": {"-1"}, 41 "Content-Length": {"0"}, 42 "Content-Encoding": {"gzip"}, 43 }, 44 map[string]bool{"Content-Length": true}, 45 "Content-Encoding: gzip\r\nExpires: -1\r\n", 46 }, 47 { 48 Header{ 49 "Expires": {"-1"}, 50 "Content-Length": {"0", "1", "2"}, 51 "Content-Encoding": {"gzip"}, 52 }, 53 map[string]bool{"Content-Length": true}, 54 "Content-Encoding: gzip\r\nExpires: -1\r\n", 55 }, 56 { 57 Header{ 58 "Expires": {"-1"}, 59 "Content-Length": {"0"}, 60 "Content-Encoding": {"gzip"}, 61 }, 62 map[string]bool{"Content-Length": true, "Expires": true, "Content-Encoding": true}, 63 "", 64 }, 65 { 66 Header{ 67 "Nil": nil, 68 "Empty": {}, 69 "Blank": {""}, 70 "Double-Blank": {"", ""}, 71 }, 72 nil, 73 "Blank: \r\nDouble-Blank: \r\nDouble-Blank: \r\n", 74 }, 75 // Tests header sorting when over the insertion sort threshold side: 76 { 77 Header{ 78 "k1": {"1a", "1b"}, 79 "k2": {"2a", "2b"}, 80 "k3": {"3a", "3b"}, 81 "k4": {"4a", "4b"}, 82 "k5": {"5a", "5b"}, 83 "k6": {"6a", "6b"}, 84 "k7": {"7a", "7b"}, 85 "k8": {"8a", "8b"}, 86 "k9": {"9a", "9b"}, 87 }, 88 map[string]bool{"k5": true}, 89 "k1: 1a\r\nk1: 1b\r\nk2: 2a\r\nk2: 2b\r\nk3: 3a\r\nk3: 3b\r\n" + 90 "k4: 4a\r\nk4: 4b\r\nk6: 6a\r\nk6: 6b\r\n" + 91 "k7: 7a\r\nk7: 7b\r\nk8: 8a\r\nk8: 8b\r\nk9: 9a\r\nk9: 9b\r\n", 92 }, 93 // Test sorting headers by the special Header-Order header 94 { 95 Header{ 96 "a": {"2"}, 97 "b": {"3"}, 98 "e": {"1"}, 99 "c": {"5"}, 100 "d": {"4"}, 101 HeaderOrderKey: {"e", "a", "b", "d", "c"}, 102 }, 103 nil, 104 "e: 1\r\na: 2\r\nb: 3\r\nd: 4\r\nc: 5\r\n", 105 }, 106 // Make sure that http 1.1 capitla letters are also sorted properly 107 { 108 Header{ 109 "X-NewRelic-ID": {"12345"}, 110 "x-api-key": {"ABCDEFGHIJKLMNOPQRSTUVWXYZ"}, 111 "MESH-Commerce-Channel": {"android-app-phone"}, 112 "mesh-version": {"cart=4"}, 113 "User-Agent": {"size/3.1.0.8355 (android-app-phone; Android 10; Build/CPH2185_11_A.28)"}, 114 "X-Request-Auth": {"hawkHeader"}, 115 "X-acf-sensor-data": {"3456"}, 116 "Content-Type": {"application/json; charset=UTF-8"}, 117 "Accept": {"application/json"}, 118 "Transfer-Encoding": {"chunked"}, 119 "Host": {"prod.jdgroupmesh.cloud"}, 120 "Connection": {"Keep-Alive"}, 121 "Accept-Encoding": {"gzip"}, 122 HeaderOrderKey: { 123 "X-NewRelic-ID", 124 "x-api-key", 125 "MESH-Commerce-Channel", 126 "mesh-version", 127 "User-Agent", 128 "X-Request-Auth", 129 "X-acf-sensor-data", 130 "Content-Type", 131 "Accept", 132 "Transfer-Encoding", 133 "Host", 134 "Connection", 135 "Accept-Encoding", 136 }, 137 }, 138 nil, 139 "X-NewRelic-ID: 12345\r\nx-api-key: ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\nMESH-Commerce-Channel: android-app-phone\r\n" + 140 "mesh-version: cart=4\r\nUser-Agent: size/3.1.0.8355 (android-app-phone; Android 10; Build/CPH2185_11_A.28)\r\n" + 141 "X-Request-Auth: hawkHeader\r\nX-acf-sensor-data: 3456\r\nContent-Type: application/json; charset=UTF-8\r\n" + 142 "Accept: application/json\r\nTransfer-Encoding: chunked\r\nHost: prod.jdgroupmesh.cloud\r\nConnection: Keep-Alive\r\n" + 143 "Accept-Encoding: gzip\r\n", 144 }, 145 } 146 147 func TestHeaderWrite(t *testing.T) { 148 var buf bytes.Buffer 149 for i, test := range headerWriteTests { 150 test.h.WriteSubset(&buf, test.exclude) 151 if buf.String() != test.expected { 152 t.Errorf("#%d:\n got: %q\nwant: %q", i, buf.String(), test.expected) 153 } 154 buf.Reset() 155 } 156 } 157 158 var parseTimeTests = []struct { 159 h Header 160 err bool 161 }{ 162 {Header{"Date": {""}}, true}, 163 {Header{"Date": {"invalid"}}, true}, 164 {Header{"Date": {"1994-11-06T08:49:37Z00:00"}}, true}, 165 {Header{"Date": {"Sun, 06 Nov 1994 08:49:37 GMT"}}, false}, 166 {Header{"Date": {"Sunday, 06-Nov-94 08:49:37 GMT"}}, false}, 167 {Header{"Date": {"Sun Nov 6 08:49:37 1994"}}, false}, 168 } 169 170 func TestParseTime(t *testing.T) { 171 expect := time.Date(1994, 11, 6, 8, 49, 37, 0, time.UTC) 172 for i, test := range parseTimeTests { 173 d, err := ParseTime(test.h.Get("Date")) 174 if err != nil { 175 if !test.err { 176 t.Errorf("#%d:\n got err: %v", i, err) 177 } 178 continue 179 } 180 if test.err { 181 t.Errorf("#%d:\n should err", i) 182 continue 183 } 184 if !expect.Equal(d) { 185 t.Errorf("#%d:\n got: %v\nwant: %v", i, d, expect) 186 } 187 } 188 } 189 190 type hasTokenTest struct { 191 header string 192 token string 193 want bool 194 } 195 196 var hasTokenTests = []hasTokenTest{ 197 {"", "", false}, 198 {"", "foo", false}, 199 {"foo", "foo", true}, 200 {"foo ", "foo", true}, 201 {" foo", "foo", true}, 202 {" foo ", "foo", true}, 203 {"foo,bar", "foo", true}, 204 {"bar,foo", "foo", true}, 205 {"bar, foo", "foo", true}, 206 {"bar,foo, baz", "foo", true}, 207 {"bar, foo,baz", "foo", true}, 208 {"bar,foo, baz", "foo", true}, 209 {"bar, foo, baz", "foo", true}, 210 {"FOO", "foo", true}, 211 {"FOO ", "foo", true}, 212 {" FOO", "foo", true}, 213 {" FOO ", "foo", true}, 214 {"FOO,BAR", "foo", true}, 215 {"BAR,FOO", "foo", true}, 216 {"BAR, FOO", "foo", true}, 217 {"BAR,FOO, baz", "foo", true}, 218 {"BAR, FOO,BAZ", "foo", true}, 219 {"BAR,FOO, BAZ", "foo", true}, 220 {"BAR, FOO, BAZ", "foo", true}, 221 {"foobar", "foo", false}, 222 {"barfoo ", "foo", false}, 223 } 224 225 func TestHasToken(t *testing.T) { 226 for _, tt := range hasTokenTests { 227 if hasToken(tt.header, tt.token) != tt.want { 228 t.Errorf("hasToken(%q, %q) = %v; want %v", tt.header, tt.token, !tt.want, tt.want) 229 } 230 } 231 } 232 233 func TestNilHeaderClone(t *testing.T) { 234 t1 := Header(nil) 235 t2 := t1.Clone() 236 if t2 != nil { 237 t.Errorf("cloned header does not match original: got: %+v; want: %+v", t2, nil) 238 } 239 } 240 241 var testHeader = Header{ 242 "Content-Length": {"123"}, 243 "Content-Type": {"text/plain"}, 244 "Date": {"some date at some time Z"}, 245 "Server": {DefaultUserAgent}, 246 } 247 248 var buf bytes.Buffer 249 250 func BenchmarkHeaderWriteSubset(b *testing.B) { 251 b.ReportAllocs() 252 for i := 0; i < b.N; i++ { 253 buf.Reset() 254 testHeader.WriteSubset(&buf, nil) 255 } 256 } 257 258 func TestHeaderWriteSubsetAllocs(t *testing.T) { 259 if testing.Short() { 260 t.Skip("skipping alloc test in short mode") 261 } 262 if race.Enabled { 263 t.Skip("skipping test under race detector") 264 } 265 if runtime.GOMAXPROCS(0) > 1 { 266 t.Skip("skipping; GOMAXPROCS>1") 267 } 268 n := testing.AllocsPerRun(100, func() { 269 buf.Reset() 270 testHeader.WriteSubset(&buf, nil) 271 }) 272 if n > 0 { 273 t.Errorf("allocs = %g; want 0", n) 274 } 275 } 276 277 // Issue 34878: test that every call to 278 // cloneOrMakeHeader never returns a nil Header. 279 func TestCloneOrMakeHeader(t *testing.T) { 280 tests := []struct { 281 name string 282 in, want Header 283 }{ 284 {"nil", nil, Header{}}, 285 {"empty", Header{}, Header{}}, 286 { 287 name: "non-empty", 288 in: Header{"foo": {"bar"}}, 289 want: Header{"foo": {"bar"}}, 290 }, 291 } 292 293 for _, tt := range tests { 294 t.Run(tt.name, func(t *testing.T) { 295 got := cloneOrMakeHeader(tt.in) 296 if got == nil { 297 t.Fatal("unexpected nil Header") 298 } 299 if !reflect.DeepEqual(got, tt.want) { 300 t.Fatalf("Got: %#v\nWant: %#v", got, tt.want) 301 } 302 got.Add("A", "B") 303 got.Get("A") 304 }) 305 } 306 } 307 308 // TestHTTP1HeaderOrder tests capitalized http1.1 header order written by request 309 func TestHTTP1HeaderOrder(t *testing.T) { 310 req, err := NewRequest("GET", "https://prod.jdgroupmesh.cloud/stores/size/products/16069871?channel=android-app-phone&expand=variations,informationBlocks,customisations", nil) 311 if err != nil { 312 t.Fatalf(err.Error()) 313 } 314 req.Header = Header{ 315 "X-NewRelic-ID": {"12345"}, 316 "x-api-key": {"ABCDE12345"}, 317 "MESH-Commerce-Channel": {"android-app-phone"}, 318 "mesh-version": {"cart=4"}, 319 "User-Agent": {"size/3.1.0.8355 (android-app-phone; Android 10; Build/CPH2185_11_A.28)"}, 320 "X-Request-Auth": {"hawkHeader"}, 321 "X-acf-sensor-data": {"3456"}, 322 "Content-Type": {"application/json; charset=UTF-8"}, 323 "Accept": {"application/json"}, 324 "Transfer-Encoding": {"chunked"}, 325 "Host": {"prod.jdgroupmesh.cloud"}, 326 "Connection": {"Keep-Alive"}, 327 "Accept-Encoding": {"gzip"}, 328 HeaderOrderKey: { 329 "x-newrelic-id", 330 "x-api-key", 331 "mesh-commerce-channel", 332 "mesh-version", 333 "user-agent", 334 "x-request-auth", 335 "x-acf-sensor-data", 336 "transfer-encoding", 337 "content-type", 338 "accept", 339 "host", 340 "connection", 341 "accept-encoding", 342 }, 343 PHeaderOrderKey: { 344 ":method", 345 ":path", 346 ":authority", 347 ":scheme", 348 }, 349 } 350 351 var b []byte 352 buf := bytes.NewBuffer(b) 353 err = req.Write(buf) 354 if err != nil { 355 t.Fatalf(err.Error()) 356 } 357 expected := "GET /stores/size/products/16069871?channel=android-app-phone&expand=variations,informationBlocks,customisations HTTP/1.1\r\nX-NewRelic-ID: 12345\r\nx-api-key: ABCDE12345\r\nMESH-Commerce-Channel: android-app-phone\r\nmesh-version: cart=4\r\nUser-Agent: size/3.1.0.8355 (android-app-phone; Android 10; Build/CPH2185_11_A.28)\r\nX-Request-Auth: hawkHeader\r\nX-acf-sensor-data: 3456\r\nTransfer-Encoding: chunked\r\nContent-Type: application/json; charset=UTF-8\r\nAccept: application/json\r\nHost: prod.jdgroupmesh.cloud\r\nConnection: Keep-Alive\r\nAccept-Encoding: gzip\r\n\r\n" 358 if expected != buf.String() { 359 t.Fatalf("got:\n%swant:\n%s", buf.String(), expected) 360 } 361 }