knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/apis/url_test.go (about) 1 /* 2 Copyright 2019 The Knative 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 apis 18 19 import ( 20 "encoding/json" 21 "strings" 22 "testing" 23 24 "github.com/google/go-cmp/cmp" 25 "k8s.io/apimachinery/pkg/api/equality" 26 ) 27 28 func TestParseURL(t *testing.T) { 29 testCases := []struct { 30 name string 31 t string 32 want *URL 33 wantEmpty bool 34 wantErr bool 35 }{{ 36 name: "empty string", 37 want: nil, 38 wantEmpty: true, 39 }, { 40 name: "invalid format", 41 t: "💩://error", 42 want: nil, 43 wantEmpty: true, 44 wantErr: true, 45 }, { 46 name: "relative", 47 t: "/path/to/something", 48 want: &URL{ 49 Path: "/path/to/something", 50 }, 51 }, { 52 name: "url", 53 t: "http://path/to/something", 54 want: &URL{ 55 Scheme: "http", 56 Host: "path", 57 Path: "/to/something", 58 }, 59 }, { 60 name: "simplehttp", 61 t: "http://foo", 62 want: HTTP("foo"), 63 }, { 64 name: "simplehttps", 65 t: "https://foo", 66 want: HTTPS("foo"), 67 }} 68 for _, tc := range testCases { 69 t.Run(tc.name, func(t *testing.T) { 70 got, err := ParseURL(tc.t) 71 if err != nil { 72 if !tc.wantErr { 73 t.Fatal("ParseURL() =", err) 74 } 75 return 76 } else if tc.wantErr { 77 t.Fatalf("ParseURL() = %v, wanted error", got) 78 } 79 80 if tc.wantEmpty != got.IsEmpty() { 81 t.Errorf("IsEmpty(%v) = %t, wanted %t", got, got.IsEmpty(), tc.wantEmpty) 82 } 83 84 if diff := cmp.Diff(tc.want, got); diff != "" { 85 t.Errorf("Unexpected object (-want, +got) =\n%s", diff) 86 } 87 }) 88 } 89 } 90 91 func TestJSONMarshalURL(t *testing.T) { 92 testCases := []struct { 93 name string 94 t string 95 want []byte 96 }{{ 97 name: "empty", 98 }, { 99 name: "empty string", 100 t: "", 101 }, { 102 name: "invalid url", 103 t: "not a url", 104 want: []byte(`"not%20a%20url"`), 105 }, { 106 name: "relative format", 107 t: "/path/to/something", 108 want: []byte(`"/path/to/something"`), 109 }} 110 for _, tc := range testCases { 111 t.Run(tc.name, func(t *testing.T) { 112 var got []byte 113 tt, err := ParseURL(tc.t) 114 if err != nil { 115 t.Fatal("ParseURL() =", err) 116 } 117 if tt != nil { 118 got, _ = tt.MarshalJSON() 119 } 120 121 if diff := cmp.Diff(tc.want, got); diff != "" { 122 t.Log("got:", string(got)) 123 t.Errorf("unexpected object (-want, +got) =\n%s", diff) 124 } 125 }) 126 } 127 } 128 129 func TestJSONUnmarshalURL(t *testing.T) { 130 testCases := map[string]struct { 131 b []byte 132 want *URL 133 wantErr string 134 }{ 135 "empty": { 136 wantErr: "unexpected end of JSON input", 137 }, 138 "invalid format": { 139 b: []byte("%"), 140 wantErr: "invalid character '%' looking for beginning of value", 141 }, 142 "relative": { 143 b: []byte(`"/path/to/something"`), 144 want: &URL{ 145 Path: "/path/to/something", 146 }, 147 }, 148 "url": { 149 b: []byte(`"http://path/to/something"`), 150 want: &URL{ 151 Scheme: "http", 152 Host: "path", 153 Path: "/to/something", 154 }, 155 }, 156 } 157 for n, tc := range testCases { 158 t.Run(n, func(t *testing.T) { 159 got := &URL{} 160 err := got.UnmarshalJSON(tc.b) 161 162 if tc.wantErr != "" || err != nil { 163 var gotErr string 164 if err != nil { 165 gotErr = err.Error() 166 } 167 if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { 168 t.Error("unexpected error (-want, +got) =", diff) 169 } 170 return 171 } 172 173 if diff := cmp.Diff(tc.want, got); diff != "" { 174 t.Error("unexpected object (-want, +got) =", diff) 175 } 176 }) 177 } 178 } 179 180 func TestJSONMarshalURLAsMember(t *testing.T) { 181 type objectType struct { 182 URL URL `json:"url,omitempty"` 183 } 184 185 testCases := map[string]struct { 186 obj *objectType 187 want []byte 188 wantErr string 189 }{ 190 "nil": { 191 want: []byte(`null`), 192 }, 193 "empty": { 194 obj: &objectType{}, 195 want: []byte(`{"url":""}`), 196 }, 197 "relative": { 198 obj: &objectType{URL: URL{Path: "/path/to/something"}}, 199 want: []byte(`{"url":"/path/to/something"}`), 200 }, 201 "url": { 202 obj: &objectType{URL: URL{ 203 Scheme: "http", 204 Host: "path", 205 Path: "/to/something", 206 }}, 207 want: []byte(`{"url":"http://path/to/something"}`), 208 }, 209 "empty url": { 210 obj: &objectType{URL: URL{}}, 211 want: []byte(`{"url":""}`), 212 }, 213 } 214 for n, tc := range testCases { 215 t.Run(n, func(t *testing.T) { 216 got, err := json.Marshal(tc.obj) 217 218 if tc.wantErr != "" || err != nil { 219 var gotErr string 220 if err != nil { 221 gotErr = err.Error() 222 } 223 if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { 224 t.Error("unexpected error (-want, +got) =", diff) 225 } 226 return 227 } 228 229 if diff := cmp.Diff(tc.want, got); diff != "" { 230 t.Error("unexpected object (-want, +got) =", diff) 231 t.Log("got:", string(got)) 232 } 233 }) 234 } 235 } 236 237 type objectType struct { 238 URL *URL `json:"url,omitempty"` 239 } 240 241 func TestJSONMarshalURLAsPointerMember(t *testing.T) { 242 testCases := map[string]struct { 243 obj *objectType 244 want []byte 245 wantErr string 246 }{ 247 "nil": { 248 want: []byte(`null`), 249 }, 250 "empty": { 251 obj: &objectType{}, 252 want: []byte(`{}`), 253 }, 254 "relative": { 255 obj: &objectType{URL: &URL{Path: "/path/to/something"}}, 256 want: []byte(`{"url":"/path/to/something"}`), 257 }, 258 "url": { 259 obj: &objectType{URL: &URL{ 260 Scheme: "http", 261 Host: "path", 262 Path: "/to/something", 263 }}, 264 want: []byte(`{"url":"http://path/to/something"}`), 265 }, 266 "empty url": { 267 obj: &objectType{URL: &URL{}}, 268 want: []byte(`{"url":""}`), 269 }, 270 } 271 for n, tc := range testCases { 272 t.Run(n, func(t *testing.T) { 273 got, err := json.Marshal(tc.obj) 274 275 if tc.wantErr != "" || err != nil { 276 var gotErr string 277 if err != nil { 278 gotErr = err.Error() 279 } 280 if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { 281 t.Error("unexpected error (-want, +got) =", diff) 282 } 283 return 284 } 285 286 if diff := cmp.Diff(tc.want, got); diff != "" { 287 t.Error("unexpected object (-want, +got) =", diff) 288 t.Log("got:", string(got)) 289 } 290 }) 291 } 292 } 293 294 func TestJSONUnmarshalURLAsMember(t *testing.T) { 295 type objectType struct { 296 URL URL `json:"url,omitempty"` 297 } 298 testCases := map[string]struct { 299 b []byte 300 want *objectType 301 wantErr string 302 }{ 303 "zero": { 304 wantErr: "unexpected end of JSON input", 305 }, 306 "empty": { 307 b: []byte(`{}`), 308 want: &objectType{}, 309 }, 310 "invalid format": { 311 b: []byte(`{"url":"%"}`), 312 wantErr: `invalid URL escape "%"`, 313 }, 314 "relative": { 315 b: []byte(`{"url":"/path/to/something"}`), 316 want: &objectType{URL: URL{Path: "/path/to/something"}}, 317 }, 318 "url": { 319 b: []byte(`{"url":"http://path/to/something"}`), 320 want: &objectType{URL: URL{ 321 Scheme: "http", 322 Host: "path", 323 Path: "/to/something", 324 }}, 325 }, 326 "empty url": { 327 b: []byte(`{"url":""}`), 328 want: &objectType{URL: URL{}}, 329 }, 330 } 331 for n, tc := range testCases { 332 t.Run(n, func(t *testing.T) { 333 got := &objectType{} 334 err := json.Unmarshal(tc.b, got) 335 336 if tc.wantErr != "" || err != nil { 337 var gotErr string 338 if err != nil { 339 gotErr = err.Error() 340 } 341 if !strings.Contains(gotErr, tc.wantErr) { 342 t.Errorf("Error `%s` does not contain wanted string `%s`", gotErr, tc.wantErr) 343 } 344 return 345 } 346 347 if diff := cmp.Diff(tc.want, got); diff != "" { 348 t.Error("Unexpected object (-want, +got) =", diff) 349 } 350 }) 351 } 352 } 353 354 func TestJSONUnmarshalURLAsMemberPointer(t *testing.T) { 355 type objectType struct { 356 URL *URL `json:"url,omitempty"` 357 } 358 testCases := []struct { 359 name string 360 b []byte 361 want *objectType 362 wantErr string 363 }{ 364 { 365 name: "zero", 366 wantErr: "unexpected end of JSON input", 367 }, { 368 name: "empty", 369 b: []byte(`{}`), 370 want: &objectType{}, 371 }, { 372 name: "invalid format", 373 b: []byte(`{"url":"%"}`), 374 wantErr: `invalid URL escape "%"`, 375 }, { 376 name: "relative", 377 b: []byte(`{"url":"/path/to/something"}`), 378 want: &objectType{URL: &URL{Path: "/path/to/something"}}, 379 }, { 380 name: "url", 381 b: []byte(`{"url":"http://path/to/something"}`), 382 want: &objectType{URL: &URL{ 383 Scheme: "http", 384 Host: "path", 385 Path: "/to/something", 386 }}, 387 }, { 388 name: "empty url", 389 b: []byte(`{"url":""}`), 390 want: &objectType{URL: &URL{}}, 391 }, 392 } 393 for _, tc := range testCases { 394 t.Run(tc.name, func(t *testing.T) { 395 got := &objectType{} 396 err := json.Unmarshal(tc.b, got) 397 398 if tc.wantErr != "" || err != nil { 399 var gotErr string 400 if err != nil { 401 gotErr = err.Error() 402 } 403 if !strings.Contains(gotErr, tc.wantErr) { 404 t.Errorf("Error `%s` does not contain wanted string `%s`", gotErr, tc.wantErr) 405 } 406 return 407 } 408 409 if diff := cmp.Diff(tc.want, got); diff != "" { 410 t.Error("Unexpected object (-want, +got) =", diff) 411 } 412 }) 413 } 414 } 415 416 func TestURLString(t *testing.T) { 417 testCases := map[string]struct { 418 t *URL 419 want string 420 }{ 421 "nil": {}, 422 "empty": { 423 t: &URL{}, 424 want: "", 425 }, 426 "relative": { 427 t: &URL{Path: "/path/to/something"}, 428 want: "/path/to/something", 429 }, 430 "nopath": { 431 t: HTTPS("foo"), 432 want: "https://foo", 433 }, 434 "absolute": { 435 t: &URL{ 436 Scheme: "http", 437 Host: "path", 438 Path: "/to/something", 439 }, 440 want: "http://path/to/something", 441 }, 442 } 443 for n, tc := range testCases { 444 t.Run(n, func(t *testing.T) { 445 got := tc.t 446 447 if diff := cmp.Diff(tc.want, got.String()); diff != "" { 448 t.Error("unexpected string (-want, +got) =", diff) 449 } 450 if diff := cmp.Diff(tc.want, got.URL().String()); diff != "" { 451 t.Error("unexpected URL (-want, +got) =", diff) 452 } 453 }) 454 } 455 } 456 457 // These are lifted from the net/url url_test.go 458 var resolveReferenceTests = []struct { 459 base, rel, expected string 460 }{ 461 // Absolute URL references 462 {"http://foo.com?a=b", "https://bar.com/", "https://bar.com/"}, 463 {"http://foo.com/", "https://bar.com/?a=b", "https://bar.com/?a=b"}, 464 {"http://foo.com/", "https://bar.com/?", "https://bar.com/?"}, 465 {"http://foo.com/bar", "mailto:foo@example.com", "mailto:foo@example.com"}, 466 467 // Path-absolute references 468 {"http://foo.com/bar", "/baz", "http://foo.com/baz"}, 469 {"http://foo.com/bar?a=b#f", "/baz", "http://foo.com/baz"}, 470 {"http://foo.com/bar?a=b", "/baz?", "http://foo.com/baz?"}, 471 {"http://foo.com/bar?a=b", "/baz?c=d", "http://foo.com/baz?c=d"}, 472 473 // Multiple slashes 474 {"http://foo.com/bar", "http://foo.com//baz", "http://foo.com//baz"}, 475 {"http://foo.com/bar", "http://foo.com///baz/quux", "http://foo.com///baz/quux"}, 476 477 // Scheme-relative 478 {"https://foo.com/bar?a=b", "//bar.com/quux", "https://bar.com/quux"}, 479 480 // Path-relative references: 481 482 // ... current directory 483 {"http://foo.com", ".", "http://foo.com/"}, 484 {"http://foo.com/bar", ".", "http://foo.com/"}, 485 {"http://foo.com/bar/", ".", "http://foo.com/bar/"}, 486 487 // ... going down 488 {"http://foo.com", "bar", "http://foo.com/bar"}, 489 {"http://foo.com/", "bar", "http://foo.com/bar"}, 490 {"http://foo.com/bar/baz", "quux", "http://foo.com/bar/quux"}, 491 492 // ... going up 493 {"http://foo.com/bar/baz", "../quux", "http://foo.com/quux"}, 494 {"http://foo.com/bar/baz", "../../../../../quux", "http://foo.com/quux"}, 495 {"http://foo.com/bar", "..", "http://foo.com/"}, 496 {"http://foo.com/bar/baz", "./..", "http://foo.com/"}, 497 // ".." in the middle (issue 3560) 498 {"http://foo.com/bar/baz", "quux/dotdot/../tail", "http://foo.com/bar/quux/tail"}, 499 {"http://foo.com/bar/baz", "quux/./dotdot/../tail", "http://foo.com/bar/quux/tail"}, 500 {"http://foo.com/bar/baz", "quux/./dotdot/.././tail", "http://foo.com/bar/quux/tail"}, 501 {"http://foo.com/bar/baz", "quux/./dotdot/./../tail", "http://foo.com/bar/quux/tail"}, 502 {"http://foo.com/bar/baz", "quux/./dotdot/dotdot/././../../tail", "http://foo.com/bar/quux/tail"}, 503 {"http://foo.com/bar/baz", "quux/./dotdot/dotdot/./.././../tail", "http://foo.com/bar/quux/tail"}, 504 {"http://foo.com/bar/baz", "quux/./dotdot/dotdot/dotdot/./../../.././././tail", "http://foo.com/bar/quux/tail"}, 505 {"http://foo.com/bar/baz", "quux/./dotdot/../dotdot/../dot/./tail/..", "http://foo.com/bar/quux/dot/"}, 506 507 // Remove any dot-segments prior to forming the target URI. 508 // http://tools.ietf.org/html/rfc3986#section-5.2.4 509 {"http://foo.com/dot/./dotdot/../foo/bar", "../baz", "http://foo.com/dot/baz"}, 510 511 // Triple dot isn't special 512 {"http://foo.com/bar", "...", "http://foo.com/..."}, 513 514 // Fragment 515 {"http://foo.com/bar", ".#frag", "http://foo.com/#frag"}, 516 {"http://example.org/", "#!$&%27()*+,;=", "http://example.org/#!$&%27()*+,;="}, 517 518 // Paths with escaping (issue 16947). 519 {"http://foo.com/foo%2fbar/", "../baz", "http://foo.com/baz"}, 520 {"http://foo.com/1/2%2f/3%2f4/5", "../../a/b/c", "http://foo.com/1/a/b/c"}, 521 {"http://foo.com/1/2/3", "./a%2f../../b/..%2fc", "http://foo.com/1/2/b/..%2fc"}, 522 {"http://foo.com/1/2%2f/3%2f4/5", "./a%2f../b/../c", "http://foo.com/1/2%2f/3%2f4/a%2f../c"}, 523 {"http://foo.com/foo%20bar/", "../baz", "http://foo.com/baz"}, 524 {"http://foo.com/foo", "../bar%2fbaz", "http://foo.com/bar%2fbaz"}, 525 {"http://foo.com/foo%2dbar/", "./baz-quux", "http://foo.com/foo%2dbar/baz-quux"}, 526 527 // RFC 3986: Normal Examples 528 // http://tools.ietf.org/html/rfc3986#section-5.4.1 529 {"http://a/b/c/d;p?q", "g:h", "g:h"}, 530 {"http://a/b/c/d;p?q", "g", "http://a/b/c/g"}, 531 {"http://a/b/c/d;p?q", "./g", "http://a/b/c/g"}, 532 {"http://a/b/c/d;p?q", "g/", "http://a/b/c/g/"}, 533 {"http://a/b/c/d;p?q", "/g", "http://a/g"}, 534 {"http://a/b/c/d;p?q", "//g", "http://g"}, 535 {"http://a/b/c/d;p?q", "?y", "http://a/b/c/d;p?y"}, 536 {"http://a/b/c/d;p?q", "g?y", "http://a/b/c/g?y"}, 537 {"http://a/b/c/d;p?q", "#s", "http://a/b/c/d;p?q#s"}, 538 {"http://a/b/c/d;p?q", "g#s", "http://a/b/c/g#s"}, 539 {"http://a/b/c/d;p?q", "g?y#s", "http://a/b/c/g?y#s"}, 540 {"http://a/b/c/d;p?q", ";x", "http://a/b/c/;x"}, 541 {"http://a/b/c/d;p?q", "g;x", "http://a/b/c/g;x"}, 542 {"http://a/b/c/d;p?q", "g;x?y#s", "http://a/b/c/g;x?y#s"}, 543 {"http://a/b/c/d;p?q", "", "http://a/b/c/d;p?q"}, 544 {"http://a/b/c/d;p?q", ".", "http://a/b/c/"}, 545 {"http://a/b/c/d;p?q", "./", "http://a/b/c/"}, 546 {"http://a/b/c/d;p?q", "..", "http://a/b/"}, 547 {"http://a/b/c/d;p?q", "../", "http://a/b/"}, 548 {"http://a/b/c/d;p?q", "../g", "http://a/b/g"}, 549 {"http://a/b/c/d;p?q", "../..", "http://a/"}, 550 {"http://a/b/c/d;p?q", "../../", "http://a/"}, 551 {"http://a/b/c/d;p?q", "../../g", "http://a/g"}, 552 553 // RFC 3986: Abnormal Examples 554 // http://tools.ietf.org/html/rfc3986#section-5.4.2 555 {"http://a/b/c/d;p?q", "../../../g", "http://a/g"}, 556 {"http://a/b/c/d;p?q", "../../../../g", "http://a/g"}, 557 {"http://a/b/c/d;p?q", "/./g", "http://a/g"}, 558 {"http://a/b/c/d;p?q", "/../g", "http://a/g"}, 559 {"http://a/b/c/d;p?q", "g.", "http://a/b/c/g."}, 560 {"http://a/b/c/d;p?q", ".g", "http://a/b/c/.g"}, 561 {"http://a/b/c/d;p?q", "g..", "http://a/b/c/g.."}, 562 {"http://a/b/c/d;p?q", "..g", "http://a/b/c/..g"}, 563 {"http://a/b/c/d;p?q", "./../g", "http://a/b/g"}, 564 {"http://a/b/c/d;p?q", "./g/.", "http://a/b/c/g/"}, 565 {"http://a/b/c/d;p?q", "g/./h", "http://a/b/c/g/h"}, 566 {"http://a/b/c/d;p?q", "g/../h", "http://a/b/c/h"}, 567 {"http://a/b/c/d;p?q", "g;x=1/./y", "http://a/b/c/g;x=1/y"}, 568 {"http://a/b/c/d;p?q", "g;x=1/../y", "http://a/b/c/y"}, 569 {"http://a/b/c/d;p?q", "g?y/./x", "http://a/b/c/g?y/./x"}, 570 {"http://a/b/c/d;p?q", "g?y/../x", "http://a/b/c/g?y/../x"}, 571 {"http://a/b/c/d;p?q", "g#s/./x", "http://a/b/c/g#s/./x"}, 572 {"http://a/b/c/d;p?q", "g#s/../x", "http://a/b/c/g#s/../x"}, 573 574 // Extras. 575 {"https://a/b/c/d;p?q", "//g?q", "https://g?q"}, 576 {"https://a/b/c/d;p?q", "//g#s", "https://g#s"}, 577 {"https://a/b/c/d;p?q", "//g/d/e/f?y#s", "https://g/d/e/f?y#s"}, 578 {"https://a/b/c/d;p#s", "?y", "https://a/b/c/d;p?y"}, 579 {"https://a/b/c/d;p?q#s", "?y", "https://a/b/c/d;p?y"}, 580 } 581 582 func TestResolveReference(t *testing.T) { 583 mustParse := func(url string) *URL { 584 u, err := ParseURL(url) 585 if err != nil { 586 t.Fatalf("ParseURL(%q) got err %v", url, err) 587 } 588 return u 589 } 590 591 for _, tc := range resolveReferenceTests { 592 apisURL := mustParse(tc.base) 593 apisRel := mustParse(tc.rel) 594 if got := apisURL.ResolveReference(apisRel).String(); got != tc.expected { 595 t.Errorf("URL(%q).ResolveReference(%q)\ngot %q\nwant %q", tc.base, tc.rel, got, tc.expected) 596 } 597 } 598 } 599 600 func TestSemanticEquality(t *testing.T) { 601 u1, err := ParseURL("https://user:password@example.com") 602 if err != nil { 603 t.Fatal("ParseURL() got err", err) 604 } 605 606 u2, err := ParseURL("https://user:password@example.com") 607 if err != nil { 608 t.Fatal("ParseURL() got err", err) 609 } 610 611 u3, err := ParseURL("https://another-user:password@example.com") 612 if err != nil { 613 t.Fatal("ParseURL() got err", err) 614 } 615 616 if !equality.Semantic.DeepEqual(u1, u2) { 617 t.Errorf("expected urls to be equivalent") 618 } 619 620 if equality.Semantic.DeepEqual(u1, u3) { 621 t.Errorf("expected urls to be different") 622 } 623 }