1
2
3
4
5 package vcs
6
7 import (
8 "errors"
9 "net/http"
10 "net/http/httptest"
11 "os"
12 "path/filepath"
13 "strings"
14 "testing"
15
16 "cmd/go/internal/web"
17 "cmd/go/internal/web/intercept"
18 )
19
20 func init() {
21
22
23
24
25 os.Setenv("GOVCS", "*:all")
26 }
27
28
29
30 func TestRepoRootForImportPath(t *testing.T) {
31
32 ts := httptest.NewServer(http.NotFoundHandler())
33 t.Cleanup(ts.Close)
34 var interceptors []intercept.Interceptor
35 for _, d := range []string{"hub.jazz.net", "hubajazz.net", "hub2.jazz.net", "different.example.com"} {
36 interceptors = append(interceptors,
37 intercept.Interceptor{Scheme: "https", FromHost: d, ToHost: ts.Listener.Addr().String()},
38 intercept.Interceptor{Scheme: "http", FromHost: d, ToHost: ts.Listener.Addr().String()},
39 )
40 }
41 intercept.EnableTestHooks(interceptors)
42 t.Cleanup(intercept.DisableTestHooks)
43
44 tests := []struct {
45 path string
46 want *RepoRoot
47 }{
48 {
49 "github.com/golang/groupcache",
50 &RepoRoot{
51 VCS: vcsGit,
52 Repo: "https://github.com/golang/groupcache",
53 },
54 },
55
56 {
57 "github.com/user/unicode/испытание",
58 nil,
59 },
60
61 {
62 "hub.jazz.net/git/user1/pkgname",
63 &RepoRoot{
64 VCS: vcsGit,
65 Repo: "https://hub.jazz.net/git/user1/pkgname",
66 },
67 },
68 {
69 "hub.jazz.net/git/user1/pkgname/submodule/submodule/submodule",
70 &RepoRoot{
71 VCS: vcsGit,
72 Repo: "https://hub.jazz.net/git/user1/pkgname",
73 },
74 },
75 {
76 "hub.jazz.net",
77 nil,
78 },
79 {
80 "hubajazz.net",
81 nil,
82 },
83 {
84 "hub2.jazz.net",
85 nil,
86 },
87 {
88 "hub.jazz.net/someotherprefix",
89 nil,
90 },
91 {
92 "hub.jazz.net/someotherprefix/user1/pkgname",
93 nil,
94 },
95
96 {
97 "hub.jazz.net/git/User 1/pkgname",
98 nil,
99 },
100 {
101 "hub.jazz.net/git/user1/pkg name",
102 nil,
103 },
104
105 {
106 "hub.jazz.net/git/user.1/pkgname",
107 nil,
108 },
109 {
110 "hub.jazz.net/git/user/pkg.name",
111 &RepoRoot{
112 VCS: vcsGit,
113 Repo: "https://hub.jazz.net/git/user/pkg.name",
114 },
115 },
116
117 {
118 "hub.jazz.net/git/USER/pkgname",
119 nil,
120 },
121
122 {
123 "git.openstack.org/openstack/swift",
124 &RepoRoot{
125 VCS: vcsGit,
126 Repo: "https://git.openstack.org/openstack/swift",
127 },
128 },
129
130
131
132 {
133 "git.openstack.org/openstack/swift.git",
134 &RepoRoot{
135 VCS: vcsGit,
136 Repo: "https://git.openstack.org/openstack/swift.git",
137 },
138 },
139 {
140 "git.openstack.org/openstack/swift/go/hummingbird",
141 &RepoRoot{
142 VCS: vcsGit,
143 Repo: "https://git.openstack.org/openstack/swift",
144 },
145 },
146 {
147 "git.openstack.org",
148 nil,
149 },
150 {
151 "git.openstack.org/openstack",
152 nil,
153 },
154
155 {
156 "git.apache.org/package name/path/to/lib",
157 nil,
158 },
159
160 {
161 "git.apache.org/package-name/path/to/lib",
162 nil,
163 },
164 {
165 "gitbapache.org",
166 nil,
167 },
168 {
169 "git.apache.org/package-name.git",
170 &RepoRoot{
171 VCS: vcsGit,
172 Repo: "https://git.apache.org/package-name.git",
173 },
174 },
175 {
176 "git.apache.org/package-name_2.x.git/path/to/lib",
177 &RepoRoot{
178 VCS: vcsGit,
179 Repo: "https://git.apache.org/package-name_2.x.git",
180 },
181 },
182 {
183 "chiselapp.com/user/kyle/repository/fossilgg",
184 &RepoRoot{
185 VCS: vcsFossil,
186 Repo: "https://chiselapp.com/user/kyle/repository/fossilgg",
187 },
188 },
189 {
190
191 "chiselapp.com/kyle/repository/fossilgg",
192 nil,
193 },
194 {
195 "chiselapp.com/user/kyle/fossilgg",
196 nil,
197 },
198 {
199 "bitbucket.org/workspace/pkgname",
200 &RepoRoot{
201 VCS: vcsGit,
202 Repo: "https://bitbucket.org/workspace/pkgname",
203 },
204 },
205 }
206
207 for _, test := range tests {
208 got, err := RepoRootForImportPath(test.path, IgnoreMod, web.SecureOnly)
209 want := test.want
210
211 if want == nil {
212 if err == nil {
213 t.Errorf("RepoRootForImportPath(%q): Error expected but not received", test.path)
214 }
215 continue
216 }
217 if err != nil {
218 t.Errorf("RepoRootForImportPath(%q): %v", test.path, err)
219 continue
220 }
221 if got.VCS.Name != want.VCS.Name || got.Repo != want.Repo {
222 t.Errorf("RepoRootForImportPath(%q) = VCS(%s) Repo(%s), want VCS(%s) Repo(%s)", test.path, got.VCS, got.Repo, want.VCS, want.Repo)
223 }
224 }
225 }
226
227
228
229 func TestFromDir(t *testing.T) {
230 tests := []struct {
231 name string
232 vcs string
233 root string
234 create func(path string) error
235 }{
236 {"hg", "Mercurial", ".hg", mkdir},
237 {"git_dir", "Git", ".git", mkdir},
238 {"git_worktree", "Git", ".git", createGitWorktreeFile},
239 {"svn", "Subversion", ".svn", mkdir},
240 {"fossil_fslckout", "Fossil", ".fslckout", touch},
241 {"fossil_FOSSIL_", "Fossil", "_FOSSIL_", touch},
242 }
243
244 for _, tt := range tests {
245 t.Run(tt.name, func(t *testing.T) {
246 tempDir := t.TempDir()
247 repoDir := filepath.Join(tempDir, "example.com")
248 if err := mkdir(repoDir); err != nil {
249 t.Fatal(err)
250 }
251 rootPath := filepath.Join(repoDir, tt.root)
252 if err := tt.create(rootPath); err != nil {
253 t.Fatal(err)
254 }
255 gotRepoDir, gotVCS, err := FromDir(repoDir, tempDir)
256 if err != nil {
257 t.Fatal(err)
258 }
259 if gotRepoDir != repoDir {
260 t.Errorf("RepoDir = %q, want %q", gotRepoDir, repoDir)
261 }
262 if gotVCS.Name != tt.vcs {
263 t.Errorf("VCS = %q, want %q", gotVCS.Name, tt.vcs)
264 }
265 })
266 }
267 }
268
269 func mkdir(path string) error {
270 return os.Mkdir(path, 0o755)
271 }
272
273 func touch(path string) error {
274 return os.WriteFile(path, nil, 0o644)
275 }
276
277 func createGitWorktreeFile(path string) error {
278 gitdir := path + ".worktree"
279
280 if err := mkdir(gitdir); err != nil {
281 return err
282 }
283 return os.WriteFile(path, []byte("gitdir: "+gitdir+"\n"), 0o644)
284 }
285
286 func TestIsSecure(t *testing.T) {
287 tests := []struct {
288 vcs *Cmd
289 url string
290 secure bool
291 }{
292 {vcsGit, "http://example.com/foo.git", false},
293 {vcsGit, "https://example.com/foo.git", true},
294 {vcsSvn, "http://example.com/svn", false},
295 {vcsSvn, "https://example.com/svn", true},
296 {vcsHg, "http://example.com/foo.hg", false},
297 {vcsHg, "https://example.com/foo.hg", true},
298 {vcsGit, "ssh://user@example.com/foo.git", true},
299 {vcsGit, "user@server:path/to/repo.git", false},
300 {vcsGit, "user@server:", false},
301 {vcsGit, "server:repo.git", false},
302 {vcsGit, "server:path/to/repo.git", false},
303 {vcsGit, "example.com:path/to/repo.git", false},
304 {vcsGit, "path/that/contains/a:colon/repo.git", false},
305 {vcsHg, "ssh://user@example.com/path/to/repo.hg", true},
306 {vcsFossil, "http://example.com/foo", false},
307 {vcsFossil, "https://example.com/foo", true},
308 }
309
310 for _, test := range tests {
311 secure := test.vcs.IsSecure(test.url)
312 if secure != test.secure {
313 t.Errorf("%s isSecure(%q) = %t; want %t", test.vcs, test.url, secure, test.secure)
314 }
315 }
316 }
317
318 func TestIsSecureGitAllowProtocol(t *testing.T) {
319 tests := []struct {
320 vcs *Cmd
321 url string
322 secure bool
323 }{
324
325 {vcsGit, "http://example.com/foo.git", false},
326 {vcsGit, "https://example.com/foo.git", true},
327 {vcsSvn, "http://example.com/svn", false},
328 {vcsSvn, "https://example.com/svn", true},
329 {vcsHg, "http://example.com/foo.hg", false},
330 {vcsHg, "https://example.com/foo.hg", true},
331 {vcsGit, "user@server:path/to/repo.git", false},
332 {vcsGit, "user@server:", false},
333 {vcsGit, "server:repo.git", false},
334 {vcsGit, "server:path/to/repo.git", false},
335 {vcsGit, "example.com:path/to/repo.git", false},
336 {vcsGit, "path/that/contains/a:colon/repo.git", false},
337 {vcsHg, "ssh://user@example.com/path/to/repo.hg", true},
338
339 {vcsGit, "ssh://user@example.com/foo.git", false},
340 {vcsGit, "foo://example.com/bar.git", true},
341 {vcsHg, "foo://example.com/bar.hg", false},
342 {vcsSvn, "foo://example.com/svn", false},
343 }
344
345 defer os.Unsetenv("GIT_ALLOW_PROTOCOL")
346 os.Setenv("GIT_ALLOW_PROTOCOL", "https:foo")
347 for _, test := range tests {
348 secure := test.vcs.IsSecure(test.url)
349 if secure != test.secure {
350 t.Errorf("%s isSecure(%q) = %t; want %t", test.vcs, test.url, secure, test.secure)
351 }
352 }
353 }
354
355 func TestMatchGoImport(t *testing.T) {
356 tests := []struct {
357 imports []metaImport
358 path string
359 mi metaImport
360 err error
361 }{
362 {
363 imports: []metaImport{
364 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
365 },
366 path: "example.com/user/foo",
367 mi: metaImport{Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
368 },
369 {
370 imports: []metaImport{
371 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
372 },
373 path: "example.com/user/foo/",
374 mi: metaImport{Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
375 },
376 {
377 imports: []metaImport{
378 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
379 {Prefix: "example.com/user/fooa", VCS: "git", RepoRoot: "https://example.com/repo/target"},
380 },
381 path: "example.com/user/foo",
382 mi: metaImport{Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
383 },
384 {
385 imports: []metaImport{
386 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
387 {Prefix: "example.com/user/fooa", VCS: "git", RepoRoot: "https://example.com/repo/target"},
388 },
389 path: "example.com/user/fooa",
390 mi: metaImport{Prefix: "example.com/user/fooa", VCS: "git", RepoRoot: "https://example.com/repo/target"},
391 },
392 {
393 imports: []metaImport{
394 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
395 {Prefix: "example.com/user/foo/bar", VCS: "git", RepoRoot: "https://example.com/repo/target"},
396 },
397 path: "example.com/user/foo/bar",
398 err: errors.New("should not be allowed to create nested repo"),
399 },
400 {
401 imports: []metaImport{
402 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
403 {Prefix: "example.com/user/foo/bar", VCS: "git", RepoRoot: "https://example.com/repo/target"},
404 },
405 path: "example.com/user/foo/bar/baz",
406 err: errors.New("should not be allowed to create nested repo"),
407 },
408 {
409 imports: []metaImport{
410 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
411 {Prefix: "example.com/user/foo/bar", VCS: "git", RepoRoot: "https://example.com/repo/target"},
412 },
413 path: "example.com/user/foo/bar/baz/qux",
414 err: errors.New("should not be allowed to create nested repo"),
415 },
416 {
417 imports: []metaImport{
418 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
419 {Prefix: "example.com/user/foo/bar", VCS: "git", RepoRoot: "https://example.com/repo/target"},
420 },
421 path: "example.com/user/foo/bar/baz/",
422 err: errors.New("should not be allowed to create nested repo"),
423 },
424 {
425 imports: []metaImport{
426 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
427 {Prefix: "example.com/user/foo/bar", VCS: "git", RepoRoot: "https://example.com/repo/target"},
428 },
429 path: "example.com",
430 err: errors.New("pathologically short path"),
431 },
432 {
433 imports: []metaImport{
434 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
435 },
436 path: "different.example.com/user/foo",
437 err: errors.New("meta tags do not match import path"),
438 },
439 {
440 imports: []metaImport{
441 {Prefix: "myitcv.io/blah2", VCS: "mod", RepoRoot: "https://raw.githubusercontent.com/myitcv/pubx/master"},
442 {Prefix: "myitcv.io", VCS: "git", RepoRoot: "https://github.com/myitcv/x"},
443 },
444 path: "myitcv.io/blah2/foo",
445 mi: metaImport{Prefix: "myitcv.io/blah2", VCS: "mod", RepoRoot: "https://raw.githubusercontent.com/myitcv/pubx/master"},
446 },
447 {
448 imports: []metaImport{
449 {Prefix: "myitcv.io/blah2", VCS: "mod", RepoRoot: "https://raw.githubusercontent.com/myitcv/pubx/master"},
450 {Prefix: "myitcv.io", VCS: "git", RepoRoot: "https://github.com/myitcv/x"},
451 },
452 path: "myitcv.io/other",
453 mi: metaImport{Prefix: "myitcv.io", VCS: "git", RepoRoot: "https://github.com/myitcv/x"},
454 },
455 {
456 imports: []metaImport{
457 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target", SubDir: "subdir"},
458 },
459 path: "example.com/user/foo",
460 mi: metaImport{Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target", SubDir: "subdir"},
461 },
462 {
463 imports: []metaImport{
464 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target", SubDir: "foo/subdir"},
465 },
466 path: "example.com/user/foo",
467 mi: metaImport{Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target", SubDir: "foo/subdir"},
468 },
469 {
470 imports: []metaImport{
471 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target", SubDir: "subdir"},
472 {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target", SubDir: ""},
473 },
474 path: "example.com/user/foo",
475 err: errors.New("multiple meta tags match import path"),
476 },
477 }
478
479 for _, test := range tests {
480 mi, err := matchGoImport(test.imports, test.path)
481 if mi != test.mi {
482 t.Errorf("unexpected metaImport; got %v, want %v", mi, test.mi)
483 }
484
485 got := err
486 want := test.err
487 if (got == nil) != (want == nil) {
488 t.Errorf("unexpected error; got %v, want %v", got, want)
489 }
490 }
491 }
492
493 func TestValidateRepoRoot(t *testing.T) {
494 tests := []struct {
495 root string
496 ok bool
497 }{
498 {
499 root: "",
500 ok: false,
501 },
502 {
503 root: "http://",
504 ok: true,
505 },
506 {
507 root: "git+ssh://",
508 ok: true,
509 },
510 {
511 root: "http#://",
512 ok: false,
513 },
514 {
515 root: "-config",
516 ok: false,
517 },
518 {
519 root: "-config://",
520 ok: false,
521 },
522 }
523
524 for _, test := range tests {
525 err := validateRepoRoot(test.root)
526 ok := err == nil
527 if ok != test.ok {
528 want := "error"
529 if test.ok {
530 want = "nil"
531 }
532 t.Errorf("validateRepoRoot(%q) = %q, want %s", test.root, err, want)
533 }
534 }
535 }
536
537 func TestValidateRepoSubDir(t *testing.T) {
538 tests := []struct {
539 subdir string
540 ok bool
541 }{
542 {
543 subdir: "",
544 ok: true,
545 },
546 {
547 subdir: "sub/dir",
548 ok: true,
549 },
550 {
551 subdir: "/leading/slash",
552 ok: false,
553 },
554 {
555 subdir: "-leading/hyphen",
556 ok: false,
557 },
558 }
559
560 for _, test := range tests {
561 err := validateRepoSubDir(test.subdir)
562 ok := err == nil
563 if ok != test.ok {
564 want := "error"
565 if test.ok {
566 want = "nil"
567 }
568 t.Errorf("validateRepoSubDir(%q) = %q, want %s", test.subdir, err, want)
569 }
570 }
571 }
572
573 var govcsTests = []struct {
574 govcs string
575 path string
576 vcs string
577 ok bool
578 }{
579 {"private:all", "is-public.com/foo", "zzz", false},
580 {"private:all", "is-private.com/foo", "zzz", true},
581 {"public:all", "is-public.com/foo", "zzz", true},
582 {"public:all", "is-private.com/foo", "zzz", false},
583 {"public:all,private:none", "is-public.com/foo", "zzz", true},
584 {"public:all,private:none", "is-private.com/foo", "zzz", false},
585 {"*:all", "is-public.com/foo", "zzz", true},
586 {"golang.org:git", "golang.org/x/text", "zzz", false},
587 {"golang.org:git", "golang.org/x/text", "git", true},
588 {"golang.org:zzz", "golang.org/x/text", "zzz", true},
589 {"golang.org:zzz", "golang.org/x/text", "git", false},
590 {"golang.org:zzz", "golang.org/x/text", "zzz", true},
591 {"golang.org:zzz", "golang.org/x/text", "git", false},
592 {"golang.org:git|hg", "golang.org/x/text", "hg", true},
593 {"golang.org:git|hg", "golang.org/x/text", "git", true},
594 {"golang.org:git|hg", "golang.org/x/text", "zzz", false},
595 {"golang.org:all", "golang.org/x/text", "hg", true},
596 {"golang.org:all", "golang.org/x/text", "git", true},
597 {"golang.org:all", "golang.org/x/text", "zzz", true},
598 {"other.xyz/p:none,golang.org/x:git", "other.xyz/p/x", "git", false},
599 {"other.xyz/p:none,golang.org/x:git", "unexpected.com", "git", false},
600 {"other.xyz/p:none,golang.org/x:git", "golang.org/x/text", "zzz", false},
601 {"other.xyz/p:none,golang.org/x:git", "golang.org/x/text", "git", true},
602 {"other.xyz/p:none,golang.org/x:zzz", "golang.org/x/text", "zzz", true},
603 {"other.xyz/p:none,golang.org/x:zzz", "golang.org/x/text", "git", false},
604 {"other.xyz/p:none,golang.org/x:git|hg", "golang.org/x/text", "hg", true},
605 {"other.xyz/p:none,golang.org/x:git|hg", "golang.org/x/text", "git", true},
606 {"other.xyz/p:none,golang.org/x:git|hg", "golang.org/x/text", "zzz", false},
607 {"other.xyz/p:none,golang.org/x:all", "golang.org/x/text", "hg", true},
608 {"other.xyz/p:none,golang.org/x:all", "golang.org/x/text", "git", true},
609 {"other.xyz/p:none,golang.org/x:all", "golang.org/x/text", "zzz", true},
610 {"other.xyz/p:none,golang.org/x:git", "golang.org/y/text", "zzz", false},
611 {"other.xyz/p:none,golang.org/x:git", "golang.org/y/text", "git", false},
612 {"other.xyz/p:none,golang.org/x:zzz", "golang.org/y/text", "zzz", false},
613 {"other.xyz/p:none,golang.org/x:zzz", "golang.org/y/text", "git", false},
614 {"other.xyz/p:none,golang.org/x:git|hg", "golang.org/y/text", "hg", false},
615 {"other.xyz/p:none,golang.org/x:git|hg", "golang.org/y/text", "git", false},
616 {"other.xyz/p:none,golang.org/x:git|hg", "golang.org/y/text", "zzz", false},
617 {"other.xyz/p:none,golang.org/x:all", "golang.org/y/text", "hg", false},
618 {"other.xyz/p:none,golang.org/x:all", "golang.org/y/text", "git", false},
619 {"other.xyz/p:none,golang.org/x:all", "golang.org/y/text", "zzz", false},
620 }
621
622 func TestGOVCS(t *testing.T) {
623 for _, tt := range govcsTests {
624 cfg, err := parseGOVCS(tt.govcs)
625 if err != nil {
626 t.Errorf("parseGOVCS(%q): %v", tt.govcs, err)
627 continue
628 }
629 private := strings.HasPrefix(tt.path, "is-private")
630 ok := cfg.allow(tt.path, private, tt.vcs)
631 if ok != tt.ok {
632 t.Errorf("parseGOVCS(%q).allow(%q, %v, %q) = %v, want %v",
633 tt.govcs, tt.path, private, tt.vcs, ok, tt.ok)
634 }
635 }
636 }
637
638 var govcsErrors = []struct {
639 s string
640 err string
641 }{
642 {`,`, `empty entry in GOVCS`},
643 {`,x`, `empty entry in GOVCS`},
644 {`x,`, `malformed entry in GOVCS (missing colon): "x"`},
645 {`x:y,`, `empty entry in GOVCS`},
646 {`x`, `malformed entry in GOVCS (missing colon): "x"`},
647 {`x:`, `empty VCS list in GOVCS: "x:"`},
648 {`x:|`, `empty VCS name in GOVCS: "x:|"`},
649 {`x:y|`, `empty VCS name in GOVCS: "x:y|"`},
650 {`x:|y`, `empty VCS name in GOVCS: "x:|y"`},
651 {`x:y,z:`, `empty VCS list in GOVCS: "z:"`},
652 {`x:y,z:|`, `empty VCS name in GOVCS: "z:|"`},
653 {`x:y,z:|w`, `empty VCS name in GOVCS: "z:|w"`},
654 {`x:y,z:w|`, `empty VCS name in GOVCS: "z:w|"`},
655 {`x:y,z:w||v`, `empty VCS name in GOVCS: "z:w||v"`},
656 {`x:y,x:z`, `unreachable pattern in GOVCS: "x:z" after "x:y"`},
657 }
658
659 func TestGOVCSErrors(t *testing.T) {
660 for _, tt := range govcsErrors {
661 _, err := parseGOVCS(tt.s)
662 if err == nil || !strings.Contains(err.Error(), tt.err) {
663 t.Errorf("parseGOVCS(%s): err=%v, want %v", tt.s, err, tt.err)
664 }
665 }
666 }
667
View as plain text