1// Copyright 2014 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 5package vcs 6 7import ( 8 "errors" 9 "fmt" 10 "internal/testenv" 11 "os" 12 "path/filepath" 13 "strings" 14 "testing" 15 16 "cmd/go/internal/web" 17) 18 19func init() { 20 // GOVCS defaults to public:git|hg,private:all, 21 // which breaks many tests here - they can't use non-git, non-hg VCS at all! 22 // Change to fully permissive. 23 // The tests of the GOVCS setting itself are in ../../testdata/script/govcs.txt. 24 os.Setenv("GOVCS", "*:all") 25} 26 27// Test that RepoRootForImportPath determines the correct RepoRoot for a given importPath. 28// TODO(cmang): Add tests for SVN and BZR. 29func TestRepoRootForImportPath(t *testing.T) { 30 testenv.MustHaveExternalNetwork(t) 31 32 tests := []struct { 33 path string 34 want *RepoRoot 35 }{ 36 { 37 "github.com/golang/groupcache", 38 &RepoRoot{ 39 VCS: vcsGit, 40 Repo: "https://github.com/golang/groupcache", 41 }, 42 }, 43 // Unicode letters in directories are not valid. 44 { 45 "github.com/user/unicode/испытание", 46 nil, 47 }, 48 // IBM DevOps Services tests 49 { 50 "hub.jazz.net/git/user1/pkgname", 51 &RepoRoot{ 52 VCS: vcsGit, 53 Repo: "https://hub.jazz.net/git/user1/pkgname", 54 }, 55 }, 56 { 57 "hub.jazz.net/git/user1/pkgname/submodule/submodule/submodule", 58 &RepoRoot{ 59 VCS: vcsGit, 60 Repo: "https://hub.jazz.net/git/user1/pkgname", 61 }, 62 }, 63 { 64 "hub.jazz.net", 65 nil, 66 }, 67 { 68 "hubajazz.net", 69 nil, 70 }, 71 { 72 "hub2.jazz.net", 73 nil, 74 }, 75 { 76 "hub.jazz.net/someotherprefix", 77 nil, 78 }, 79 { 80 "hub.jazz.net/someotherprefix/user1/pkgname", 81 nil, 82 }, 83 // Spaces are not valid in user names or package names 84 { 85 "hub.jazz.net/git/User 1/pkgname", 86 nil, 87 }, 88 { 89 "hub.jazz.net/git/user1/pkg name", 90 nil, 91 }, 92 // Dots are not valid in user names 93 { 94 "hub.jazz.net/git/user.1/pkgname", 95 nil, 96 }, 97 { 98 "hub.jazz.net/git/user/pkg.name", 99 &RepoRoot{ 100 VCS: vcsGit, 101 Repo: "https://hub.jazz.net/git/user/pkg.name", 102 }, 103 }, 104 // User names cannot have uppercase letters 105 { 106 "hub.jazz.net/git/USER/pkgname", 107 nil, 108 }, 109 // OpenStack tests 110 { 111 "git.openstack.org/openstack/swift", 112 &RepoRoot{ 113 VCS: vcsGit, 114 Repo: "https://git.openstack.org/openstack/swift", 115 }, 116 }, 117 // Trailing .git is less preferred but included for 118 // compatibility purposes while the same source needs to 119 // be compilable on both old and new go 120 { 121 "git.openstack.org/openstack/swift.git", 122 &RepoRoot{ 123 VCS: vcsGit, 124 Repo: "https://git.openstack.org/openstack/swift.git", 125 }, 126 }, 127 { 128 "git.openstack.org/openstack/swift/go/hummingbird", 129 &RepoRoot{ 130 VCS: vcsGit, 131 Repo: "https://git.openstack.org/openstack/swift", 132 }, 133 }, 134 { 135 "git.openstack.org", 136 nil, 137 }, 138 { 139 "git.openstack.org/openstack", 140 nil, 141 }, 142 // Spaces are not valid in package name 143 { 144 "git.apache.org/package name/path/to/lib", 145 nil, 146 }, 147 // Should have ".git" suffix 148 { 149 "git.apache.org/package-name/path/to/lib", 150 nil, 151 }, 152 { 153 "gitbapache.org", 154 nil, 155 }, 156 { 157 "git.apache.org/package-name.git", 158 &RepoRoot{ 159 VCS: vcsGit, 160 Repo: "https://git.apache.org/package-name.git", 161 }, 162 }, 163 { 164 "git.apache.org/package-name_2.x.git/path/to/lib", 165 &RepoRoot{ 166 VCS: vcsGit, 167 Repo: "https://git.apache.org/package-name_2.x.git", 168 }, 169 }, 170 { 171 "chiselapp.com/user/kyle/repository/fossilgg", 172 &RepoRoot{ 173 VCS: vcsFossil, 174 Repo: "https://chiselapp.com/user/kyle/repository/fossilgg", 175 }, 176 }, 177 { 178 // must have a user/$name/repository/$repo path 179 "chiselapp.com/kyle/repository/fossilgg", 180 nil, 181 }, 182 { 183 "chiselapp.com/user/kyle/fossilgg", 184 nil, 185 }, 186 { 187 "bitbucket.org/workspace/pkgname", 188 &RepoRoot{ 189 VCS: vcsGit, 190 Repo: "https://bitbucket.org/workspace/pkgname", 191 }, 192 }, 193 } 194 195 for _, test := range tests { 196 got, err := RepoRootForImportPath(test.path, IgnoreMod, web.SecureOnly) 197 want := test.want 198 199 if want == nil { 200 if err == nil { 201 t.Errorf("RepoRootForImportPath(%q): Error expected but not received", test.path) 202 } 203 continue 204 } 205 if err != nil { 206 t.Errorf("RepoRootForImportPath(%q): %v", test.path, err) 207 continue 208 } 209 if got.VCS.Name != want.VCS.Name || got.Repo != want.Repo { 210 t.Errorf("RepoRootForImportPath(%q) = VCS(%s) Repo(%s), want VCS(%s) Repo(%s)", test.path, got.VCS, got.Repo, want.VCS, want.Repo) 211 } 212 } 213} 214 215// Test that vcs.FromDir correctly inspects a given directory and returns the 216// right VCS and repo directory. 217func TestFromDir(t *testing.T) { 218 tempDir := t.TempDir() 219 220 for _, vcs := range vcsList { 221 for r, root := range vcs.RootNames { 222 vcsName := fmt.Sprint(vcs.Name, r) 223 dir := filepath.Join(tempDir, "example.com", vcsName, root.filename) 224 if root.isDir { 225 err := os.MkdirAll(dir, 0755) 226 if err != nil { 227 t.Fatal(err) 228 } 229 } else { 230 err := os.MkdirAll(filepath.Dir(dir), 0755) 231 if err != nil { 232 t.Fatal(err) 233 } 234 f, err := os.Create(dir) 235 if err != nil { 236 t.Fatal(err) 237 } 238 f.Close() 239 } 240 241 wantRepoDir := filepath.Dir(dir) 242 gotRepoDir, gotVCS, err := FromDir(dir, tempDir, false) 243 if err != nil { 244 t.Errorf("FromDir(%q, %q): %v", dir, tempDir, err) 245 continue 246 } 247 if gotRepoDir != wantRepoDir || gotVCS.Name != vcs.Name { 248 t.Errorf("FromDir(%q, %q) = RepoDir(%s), VCS(%s); want RepoDir(%s), VCS(%s)", dir, tempDir, gotRepoDir, gotVCS.Name, wantRepoDir, vcs.Name) 249 } 250 } 251 } 252} 253 254func TestIsSecure(t *testing.T) { 255 tests := []struct { 256 vcs *Cmd 257 url string 258 secure bool 259 }{ 260 {vcsGit, "http://example.com/foo.git", false}, 261 {vcsGit, "https://example.com/foo.git", true}, 262 {vcsBzr, "http://example.com/foo.bzr", false}, 263 {vcsBzr, "https://example.com/foo.bzr", true}, 264 {vcsSvn, "http://example.com/svn", false}, 265 {vcsSvn, "https://example.com/svn", true}, 266 {vcsHg, "http://example.com/foo.hg", false}, 267 {vcsHg, "https://example.com/foo.hg", true}, 268 {vcsGit, "ssh://[email protected]/foo.git", true}, 269 {vcsGit, "user@server:path/to/repo.git", false}, 270 {vcsGit, "user@server:", false}, 271 {vcsGit, "server:repo.git", false}, 272 {vcsGit, "server:path/to/repo.git", false}, 273 {vcsGit, "example.com:path/to/repo.git", false}, 274 {vcsGit, "path/that/contains/a:colon/repo.git", false}, 275 {vcsHg, "ssh://[email protected]/path/to/repo.hg", true}, 276 {vcsFossil, "http://example.com/foo", false}, 277 {vcsFossil, "https://example.com/foo", true}, 278 } 279 280 for _, test := range tests { 281 secure := test.vcs.IsSecure(test.url) 282 if secure != test.secure { 283 t.Errorf("%s isSecure(%q) = %t; want %t", test.vcs, test.url, secure, test.secure) 284 } 285 } 286} 287 288func TestIsSecureGitAllowProtocol(t *testing.T) { 289 tests := []struct { 290 vcs *Cmd 291 url string 292 secure bool 293 }{ 294 // Same as TestIsSecure to verify same behavior. 295 {vcsGit, "http://example.com/foo.git", false}, 296 {vcsGit, "https://example.com/foo.git", true}, 297 {vcsBzr, "http://example.com/foo.bzr", false}, 298 {vcsBzr, "https://example.com/foo.bzr", true}, 299 {vcsSvn, "http://example.com/svn", false}, 300 {vcsSvn, "https://example.com/svn", true}, 301 {vcsHg, "http://example.com/foo.hg", false}, 302 {vcsHg, "https://example.com/foo.hg", true}, 303 {vcsGit, "user@server:path/to/repo.git", false}, 304 {vcsGit, "user@server:", false}, 305 {vcsGit, "server:repo.git", false}, 306 {vcsGit, "server:path/to/repo.git", false}, 307 {vcsGit, "example.com:path/to/repo.git", false}, 308 {vcsGit, "path/that/contains/a:colon/repo.git", false}, 309 {vcsHg, "ssh://[email protected]/path/to/repo.hg", true}, 310 // New behavior. 311 {vcsGit, "ssh://[email protected]/foo.git", false}, 312 {vcsGit, "foo://example.com/bar.git", true}, 313 {vcsHg, "foo://example.com/bar.hg", false}, 314 {vcsSvn, "foo://example.com/svn", false}, 315 {vcsBzr, "foo://example.com/bar.bzr", false}, 316 } 317 318 defer os.Unsetenv("GIT_ALLOW_PROTOCOL") 319 os.Setenv("GIT_ALLOW_PROTOCOL", "https:foo") 320 for _, test := range tests { 321 secure := test.vcs.IsSecure(test.url) 322 if secure != test.secure { 323 t.Errorf("%s isSecure(%q) = %t; want %t", test.vcs, test.url, secure, test.secure) 324 } 325 } 326} 327 328func TestMatchGoImport(t *testing.T) { 329 tests := []struct { 330 imports []metaImport 331 path string 332 mi metaImport 333 err error 334 }{ 335 { 336 imports: []metaImport{ 337 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 338 }, 339 path: "example.com/user/foo", 340 mi: metaImport{Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 341 }, 342 { 343 imports: []metaImport{ 344 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 345 }, 346 path: "example.com/user/foo/", 347 mi: metaImport{Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 348 }, 349 { 350 imports: []metaImport{ 351 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 352 {Prefix: "example.com/user/fooa", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 353 }, 354 path: "example.com/user/foo", 355 mi: metaImport{Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 356 }, 357 { 358 imports: []metaImport{ 359 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 360 {Prefix: "example.com/user/fooa", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 361 }, 362 path: "example.com/user/fooa", 363 mi: metaImport{Prefix: "example.com/user/fooa", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 364 }, 365 { 366 imports: []metaImport{ 367 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 368 {Prefix: "example.com/user/foo/bar", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 369 }, 370 path: "example.com/user/foo/bar", 371 err: errors.New("should not be allowed to create nested repo"), 372 }, 373 { 374 imports: []metaImport{ 375 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 376 {Prefix: "example.com/user/foo/bar", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 377 }, 378 path: "example.com/user/foo/bar/baz", 379 err: errors.New("should not be allowed to create nested repo"), 380 }, 381 { 382 imports: []metaImport{ 383 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 384 {Prefix: "example.com/user/foo/bar", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 385 }, 386 path: "example.com/user/foo/bar/baz/qux", 387 err: errors.New("should not be allowed to create nested repo"), 388 }, 389 { 390 imports: []metaImport{ 391 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 392 {Prefix: "example.com/user/foo/bar", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 393 }, 394 path: "example.com/user/foo/bar/baz/", 395 err: errors.New("should not be allowed to create nested repo"), 396 }, 397 { 398 imports: []metaImport{ 399 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 400 {Prefix: "example.com/user/foo/bar", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 401 }, 402 path: "example.com", 403 err: errors.New("pathologically short path"), 404 }, 405 { 406 imports: []metaImport{ 407 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"}, 408 }, 409 path: "different.example.com/user/foo", 410 err: errors.New("meta tags do not match import path"), 411 }, 412 { 413 imports: []metaImport{ 414 {Prefix: "myitcv.io/blah2", VCS: "mod", RepoRoot: "https://raw.githubusercontent.com/myitcv/pubx/master"}, 415 {Prefix: "myitcv.io", VCS: "git", RepoRoot: "https://github.com/myitcv/x"}, 416 }, 417 path: "myitcv.io/blah2/foo", 418 mi: metaImport{Prefix: "myitcv.io/blah2", VCS: "mod", RepoRoot: "https://raw.githubusercontent.com/myitcv/pubx/master"}, 419 }, 420 { 421 imports: []metaImport{ 422 {Prefix: "myitcv.io/blah2", VCS: "mod", RepoRoot: "https://raw.githubusercontent.com/myitcv/pubx/master"}, 423 {Prefix: "myitcv.io", VCS: "git", RepoRoot: "https://github.com/myitcv/x"}, 424 }, 425 path: "myitcv.io/other", 426 mi: metaImport{Prefix: "myitcv.io", VCS: "git", RepoRoot: "https://github.com/myitcv/x"}, 427 }, 428 } 429 430 for _, test := range tests { 431 mi, err := matchGoImport(test.imports, test.path) 432 if mi != test.mi { 433 t.Errorf("unexpected metaImport; got %v, want %v", mi, test.mi) 434 } 435 436 got := err 437 want := test.err 438 if (got == nil) != (want == nil) { 439 t.Errorf("unexpected error; got %v, want %v", got, want) 440 } 441 } 442} 443 444func TestValidateRepoRoot(t *testing.T) { 445 tests := []struct { 446 root string 447 ok bool 448 }{ 449 { 450 root: "", 451 ok: false, 452 }, 453 { 454 root: "http://", 455 ok: true, 456 }, 457 { 458 root: "git+ssh://", 459 ok: true, 460 }, 461 { 462 root: "http#://", 463 ok: false, 464 }, 465 { 466 root: "-config", 467 ok: false, 468 }, 469 { 470 root: "-config://", 471 ok: false, 472 }, 473 } 474 475 for _, test := range tests { 476 err := validateRepoRoot(test.root) 477 ok := err == nil 478 if ok != test.ok { 479 want := "error" 480 if test.ok { 481 want = "nil" 482 } 483 t.Errorf("validateRepoRoot(%q) = %q, want %s", test.root, err, want) 484 } 485 } 486} 487 488var govcsTests = []struct { 489 govcs string 490 path string 491 vcs string 492 ok bool 493}{ 494 {"private:all", "is-public.com/foo", "zzz", false}, 495 {"private:all", "is-private.com/foo", "zzz", true}, 496 {"public:all", "is-public.com/foo", "zzz", true}, 497 {"public:all", "is-private.com/foo", "zzz", false}, 498 {"public:all,private:none", "is-public.com/foo", "zzz", true}, 499 {"public:all,private:none", "is-private.com/foo", "zzz", false}, 500 {"*:all", "is-public.com/foo", "zzz", true}, 501 {"golang.org:git", "golang.org/x/text", "zzz", false}, 502 {"golang.org:git", "golang.org/x/text", "git", true}, 503 {"golang.org:zzz", "golang.org/x/text", "zzz", true}, 504 {"golang.org:zzz", "golang.org/x/text", "git", false}, 505 {"golang.org:zzz", "golang.org/x/text", "zzz", true}, 506 {"golang.org:zzz", "golang.org/x/text", "git", false}, 507 {"golang.org:git|hg", "golang.org/x/text", "hg", true}, 508 {"golang.org:git|hg", "golang.org/x/text", "git", true}, 509 {"golang.org:git|hg", "golang.org/x/text", "zzz", false}, 510 {"golang.org:all", "golang.org/x/text", "hg", true}, 511 {"golang.org:all", "golang.org/x/text", "git", true}, 512 {"golang.org:all", "golang.org/x/text", "zzz", true}, 513 {"other.xyz/p:none,golang.org/x:git", "other.xyz/p/x", "git", false}, 514 {"other.xyz/p:none,golang.org/x:git", "unexpected.com", "git", false}, 515 {"other.xyz/p:none,golang.org/x:git", "golang.org/x/text", "zzz", false}, 516 {"other.xyz/p:none,golang.org/x:git", "golang.org/x/text", "git", true}, 517 {"other.xyz/p:none,golang.org/x:zzz", "golang.org/x/text", "zzz", true}, 518 {"other.xyz/p:none,golang.org/x:zzz", "golang.org/x/text", "git", false}, 519 {"other.xyz/p:none,golang.org/x:git|hg", "golang.org/x/text", "hg", true}, 520 {"other.xyz/p:none,golang.org/x:git|hg", "golang.org/x/text", "git", true}, 521 {"other.xyz/p:none,golang.org/x:git|hg", "golang.org/x/text", "zzz", false}, 522 {"other.xyz/p:none,golang.org/x:all", "golang.org/x/text", "hg", true}, 523 {"other.xyz/p:none,golang.org/x:all", "golang.org/x/text", "git", true}, 524 {"other.xyz/p:none,golang.org/x:all", "golang.org/x/text", "zzz", true}, 525 {"other.xyz/p:none,golang.org/x:git", "golang.org/y/text", "zzz", false}, 526 {"other.xyz/p:none,golang.org/x:git", "golang.org/y/text", "git", false}, 527 {"other.xyz/p:none,golang.org/x:zzz", "golang.org/y/text", "zzz", false}, 528 {"other.xyz/p:none,golang.org/x:zzz", "golang.org/y/text", "git", false}, 529 {"other.xyz/p:none,golang.org/x:git|hg", "golang.org/y/text", "hg", false}, 530 {"other.xyz/p:none,golang.org/x:git|hg", "golang.org/y/text", "git", false}, 531 {"other.xyz/p:none,golang.org/x:git|hg", "golang.org/y/text", "zzz", false}, 532 {"other.xyz/p:none,golang.org/x:all", "golang.org/y/text", "hg", false}, 533 {"other.xyz/p:none,golang.org/x:all", "golang.org/y/text", "git", false}, 534 {"other.xyz/p:none,golang.org/x:all", "golang.org/y/text", "zzz", false}, 535} 536 537func TestGOVCS(t *testing.T) { 538 for _, tt := range govcsTests { 539 cfg, err := parseGOVCS(tt.govcs) 540 if err != nil { 541 t.Errorf("parseGOVCS(%q): %v", tt.govcs, err) 542 continue 543 } 544 private := strings.HasPrefix(tt.path, "is-private") 545 ok := cfg.allow(tt.path, private, tt.vcs) 546 if ok != tt.ok { 547 t.Errorf("parseGOVCS(%q).allow(%q, %v, %q) = %v, want %v", 548 tt.govcs, tt.path, private, tt.vcs, ok, tt.ok) 549 } 550 } 551} 552 553var govcsErrors = []struct { 554 s string 555 err string 556}{ 557 {`,`, `empty entry in GOVCS`}, 558 {`,x`, `empty entry in GOVCS`}, 559 {`x,`, `malformed entry in GOVCS (missing colon): "x"`}, 560 {`x:y,`, `empty entry in GOVCS`}, 561 {`x`, `malformed entry in GOVCS (missing colon): "x"`}, 562 {`x:`, `empty VCS list in GOVCS: "x:"`}, 563 {`x:|`, `empty VCS name in GOVCS: "x:|"`}, 564 {`x:y|`, `empty VCS name in GOVCS: "x:y|"`}, 565 {`x:|y`, `empty VCS name in GOVCS: "x:|y"`}, 566 {`x:y,z:`, `empty VCS list in GOVCS: "z:"`}, 567 {`x:y,z:|`, `empty VCS name in GOVCS: "z:|"`}, 568 {`x:y,z:|w`, `empty VCS name in GOVCS: "z:|w"`}, 569 {`x:y,z:w|`, `empty VCS name in GOVCS: "z:w|"`}, 570 {`x:y,z:w||v`, `empty VCS name in GOVCS: "z:w||v"`}, 571 {`x:y,x:z`, `unreachable pattern in GOVCS: "x:z" after "x:y"`}, 572} 573 574func TestGOVCSErrors(t *testing.T) { 575 for _, tt := range govcsErrors { 576 _, err := parseGOVCS(tt.s) 577 if err == nil || !strings.Contains(err.Error(), tt.err) { 578 t.Errorf("parseGOVCS(%s): err=%v, want %v", tt.s, err, tt.err) 579 } 580 } 581} 582