github.com/opentofu/opentofu@v1.7.1/internal/addrs/module_source_test.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package addrs 7 8 import ( 9 "runtime" 10 "testing" 11 12 "github.com/google/go-cmp/cmp" 13 svchost "github.com/hashicorp/terraform-svchost" 14 ) 15 16 func TestParseModuleSource(t *testing.T) { 17 18 absolutePath, absolutePathModulePackage := testDataAbsolutePath() 19 absolutePathSubdir, absolutePathSubdirModulePackage := testDataAbsolutePathSubdir() 20 21 tests := map[string]struct { 22 input string 23 want ModuleSource 24 wantErr string 25 }{ 26 // Local paths 27 "local in subdirectory": { 28 input: "./child", 29 want: ModuleSourceLocal("./child"), 30 }, 31 "local in subdirectory non-normalized": { 32 input: "./nope/../child", 33 want: ModuleSourceLocal("./child"), 34 }, 35 "local in sibling directory": { 36 input: "../sibling", 37 want: ModuleSourceLocal("../sibling"), 38 }, 39 "local in sibling directory non-normalized": { 40 input: "./nope/../../sibling", 41 want: ModuleSourceLocal("../sibling"), 42 }, 43 "Windows-style local in subdirectory": { 44 input: `.\child`, 45 want: ModuleSourceLocal("./child"), 46 }, 47 "Windows-style local in subdirectory non-normalized": { 48 input: `.\nope\..\child`, 49 want: ModuleSourceLocal("./child"), 50 }, 51 "Windows-style local in sibling directory": { 52 input: `..\sibling`, 53 want: ModuleSourceLocal("../sibling"), 54 }, 55 "Windows-style local in sibling directory non-normalized": { 56 input: `.\nope\..\..\sibling`, 57 want: ModuleSourceLocal("../sibling"), 58 }, 59 "an abominable mix of different slashes": { 60 input: `./nope\nope/why\./please\don't`, 61 want: ModuleSourceLocal("./nope/nope/why/please/don't"), 62 }, 63 64 // Registry addresses 65 // (NOTE: There is another test function TestParseModuleSourceRegistry 66 // which tests this situation more exhaustively, so this is just a 67 // token set of cases to see that we are indeed calling into the 68 // registry address parser when appropriate.) 69 "main registry implied": { 70 input: "hashicorp/subnets/cidr", 71 want: ModuleSourceRegistry{ 72 Package: ModuleRegistryPackage{ 73 Host: svchost.Hostname("registry.opentofu.org"), 74 Namespace: "hashicorp", 75 Name: "subnets", 76 TargetSystem: "cidr", 77 }, 78 Subdir: "", 79 }, 80 }, 81 "main registry implied, subdir": { 82 input: "hashicorp/subnets/cidr//examples/foo", 83 want: ModuleSourceRegistry{ 84 Package: ModuleRegistryPackage{ 85 Host: svchost.Hostname("registry.opentofu.org"), 86 Namespace: "hashicorp", 87 Name: "subnets", 88 TargetSystem: "cidr", 89 }, 90 Subdir: "examples/foo", 91 }, 92 }, 93 "main registry implied, escaping subdir": { 94 input: "hashicorp/subnets/cidr//../nope", 95 // NOTE: This error is actually being caught by the _remote package_ 96 // address parser, because any registry parsing failure falls back 97 // to that but both of them have the same subdir validation. This 98 // case is here to make sure that stays true, so we keep reporting 99 // a suitable error when the user writes a registry-looking thing. 100 wantErr: `subdirectory path "../nope" leads outside of the module package`, 101 }, 102 "custom registry": { 103 input: "example.com/awesomecorp/network/happycloud", 104 want: ModuleSourceRegistry{ 105 Package: ModuleRegistryPackage{ 106 Host: svchost.Hostname("example.com"), 107 Namespace: "awesomecorp", 108 Name: "network", 109 TargetSystem: "happycloud", 110 }, 111 Subdir: "", 112 }, 113 }, 114 "custom registry, subdir": { 115 input: "example.com/awesomecorp/network/happycloud//examples/foo", 116 want: ModuleSourceRegistry{ 117 Package: ModuleRegistryPackage{ 118 Host: svchost.Hostname("example.com"), 119 Namespace: "awesomecorp", 120 Name: "network", 121 TargetSystem: "happycloud", 122 }, 123 Subdir: "examples/foo", 124 }, 125 }, 126 127 // Remote package addresses 128 "github.com shorthand": { 129 input: "github.com/hashicorp/terraform-cidr-subnets", 130 want: ModuleSourceRemote{ 131 Package: ModulePackage("git::https://github.com/hashicorp/terraform-cidr-subnets.git"), 132 }, 133 }, 134 "github.com shorthand, subdir": { 135 input: "github.com/hashicorp/terraform-cidr-subnets//example/foo", 136 want: ModuleSourceRemote{ 137 Package: ModulePackage("git::https://github.com/hashicorp/terraform-cidr-subnets.git"), 138 Subdir: "example/foo", 139 }, 140 }, 141 "git protocol, URL-style": { 142 input: "git://example.com/code/baz.git", 143 want: ModuleSourceRemote{ 144 Package: ModulePackage("git://example.com/code/baz.git"), 145 }, 146 }, 147 "git protocol, URL-style, subdir": { 148 input: "git://example.com/code/baz.git//bleep/bloop", 149 want: ModuleSourceRemote{ 150 Package: ModulePackage("git://example.com/code/baz.git"), 151 Subdir: "bleep/bloop", 152 }, 153 }, 154 "git over HTTPS, URL-style": { 155 input: "git::https://example.com/code/baz.git", 156 want: ModuleSourceRemote{ 157 Package: ModulePackage("git::https://example.com/code/baz.git"), 158 }, 159 }, 160 "git over HTTPS, URL-style, subdir": { 161 input: "git::https://example.com/code/baz.git//bleep/bloop", 162 want: ModuleSourceRemote{ 163 Package: ModulePackage("git::https://example.com/code/baz.git"), 164 Subdir: "bleep/bloop", 165 }, 166 }, 167 "git over HTTPS, URL-style, subdir, query parameters": { 168 input: "git::https://example.com/code/baz.git//bleep/bloop?otherthing=blah", 169 want: ModuleSourceRemote{ 170 Package: ModulePackage("git::https://example.com/code/baz.git?otherthing=blah"), 171 Subdir: "bleep/bloop", 172 }, 173 }, 174 "git over SSH, URL-style": { 175 input: "git::ssh://git@example.com/code/baz.git", 176 want: ModuleSourceRemote{ 177 Package: ModulePackage("git::ssh://git@example.com/code/baz.git"), 178 }, 179 }, 180 "git over SSH, URL-style, subdir": { 181 input: "git::ssh://git@example.com/code/baz.git//bleep/bloop", 182 want: ModuleSourceRemote{ 183 Package: ModulePackage("git::ssh://git@example.com/code/baz.git"), 184 Subdir: "bleep/bloop", 185 }, 186 }, 187 "git over SSH, scp-style": { 188 input: "git::git@example.com:code/baz.git", 189 want: ModuleSourceRemote{ 190 // Normalized to URL-style 191 Package: ModulePackage("git::ssh://git@example.com/code/baz.git"), 192 }, 193 }, 194 "git over SSH, scp-style, subdir": { 195 input: "git::git@example.com:code/baz.git//bleep/bloop", 196 want: ModuleSourceRemote{ 197 // Normalized to URL-style 198 Package: ModulePackage("git::ssh://git@example.com/code/baz.git"), 199 Subdir: "bleep/bloop", 200 }, 201 }, 202 203 // NOTE: We intentionally don't test the bitbucket.org shorthands 204 // here, because that detector makes direct HTTP tequests to the 205 // Bitbucket API and thus isn't appropriate for unit testing. 206 207 "Google Cloud Storage bucket implied, path prefix": { 208 input: "www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE", 209 want: ModuleSourceRemote{ 210 Package: ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE"), 211 }, 212 }, 213 "Google Cloud Storage bucket, path prefix": { 214 input: "gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE", 215 want: ModuleSourceRemote{ 216 Package: ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE"), 217 }, 218 }, 219 "Google Cloud Storage bucket implied, archive object": { 220 input: "www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip", 221 want: ModuleSourceRemote{ 222 Package: ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip"), 223 }, 224 }, 225 "Google Cloud Storage bucket, archive object": { 226 input: "gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip", 227 want: ModuleSourceRemote{ 228 Package: ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip"), 229 }, 230 }, 231 232 "Amazon S3 bucket implied, archive object": { 233 input: "s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip", 234 want: ModuleSourceRemote{ 235 Package: ModulePackage("s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip"), 236 }, 237 }, 238 "Amazon S3 bucket, archive object": { 239 input: "s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip", 240 want: ModuleSourceRemote{ 241 Package: ModulePackage("s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip"), 242 }, 243 }, 244 245 "HTTP URL": { 246 input: "http://example.com/module", 247 want: ModuleSourceRemote{ 248 Package: ModulePackage("http://example.com/module"), 249 }, 250 }, 251 "HTTPS URL": { 252 input: "https://example.com/module", 253 want: ModuleSourceRemote{ 254 Package: ModulePackage("https://example.com/module"), 255 }, 256 }, 257 "HTTPS URL, archive file": { 258 input: "https://example.com/module.zip", 259 want: ModuleSourceRemote{ 260 Package: ModulePackage("https://example.com/module.zip"), 261 }, 262 }, 263 "HTTPS URL, forced archive file": { 264 input: "https://example.com/module?archive=tar", 265 want: ModuleSourceRemote{ 266 Package: ModulePackage("https://example.com/module?archive=tar"), 267 }, 268 }, 269 "HTTPS URL, forced archive file and checksum": { 270 input: "https://example.com/module?archive=tar&checksum=blah", 271 want: ModuleSourceRemote{ 272 // The query string only actually gets processed when we finally 273 // do the get, so "checksum=blah" is accepted as valid up 274 // at this parsing layer. 275 Package: ModulePackage("https://example.com/module?archive=tar&checksum=blah"), 276 }, 277 }, 278 "absolute filesystem path": { 279 // Although a local directory isn't really "remote", we do 280 // treat it as such because we still need to do all of the same 281 // high-level steps to work with these, even though "downloading" 282 // is replaced by a deep filesystem copy instead. 283 input: absolutePath, 284 want: ModuleSourceRemote{ 285 Package: ModulePackage(absolutePathModulePackage), 286 }, 287 }, 288 "absolute filesystem path, subdir": { 289 // This is a funny situation where the user wants to use a 290 // directory elsewhere on their system as a package containing 291 // multiple modules, but the entry point is not at the root 292 // of that subtree, and so they can use the usual subdir 293 // syntax to move the package root higher in the real filesystem. 294 input: absolutePathSubdir, 295 want: ModuleSourceRemote{ 296 Package: ModulePackage(absolutePathSubdirModulePackage), 297 Subdir: "example", 298 }, 299 }, 300 301 "subdir escaping out of package": { 302 // This is general logic for all subdir regardless of installation 303 // protocol, but we're using a filesystem path here just as an 304 // easy placeholder/ 305 input: "/tmp/foo//example/../../invalid", 306 wantErr: `subdirectory path "../invalid" leads outside of the module package`, 307 }, 308 309 "relative path without the needed prefix": { 310 input: "boop/bloop", 311 // For this case we return a generic error message from the addrs 312 // layer, but using a specialized error type which our module 313 // installer checks for and produces an extra hint for users who 314 // were intending to write a local path which then got 315 // misinterpreted as a remote source due to the missing prefix. 316 // However, the main message is generic here because this is really 317 // just a general "this string doesn't match any of our source 318 // address patterns" situation, not _necessarily_ about relative 319 // local paths. 320 wantErr: `OpenTofu cannot detect a supported external module source type for boop/bloop`, 321 }, 322 323 "go-getter will accept all sorts of garbage": { 324 input: "dfgdfgsd:dgfhdfghdfghdfg/dfghdfghdfg", 325 want: ModuleSourceRemote{ 326 // Unfortunately go-getter doesn't actually reject a totally 327 // invalid address like this until getting time, as long as 328 // it looks somewhat like a URL. 329 Package: ModulePackage("dfgdfgsd:dgfhdfghdfghdfg/dfghdfghdfg"), 330 }, 331 }, 332 } 333 334 for name, test := range tests { 335 t.Run(name, func(t *testing.T) { 336 addr, err := ParseModuleSource(test.input) 337 338 if test.wantErr != "" { 339 switch { 340 case err == nil: 341 t.Errorf("unexpected success\nwant error: %s", test.wantErr) 342 case err.Error() != test.wantErr: 343 t.Errorf("wrong error messages\ngot: %s\nwant: %s", err.Error(), test.wantErr) 344 } 345 return 346 } 347 348 if err != nil { 349 t.Fatalf("unexpected error: %s", err.Error()) 350 } 351 352 if diff := cmp.Diff(addr, test.want); diff != "" { 353 t.Errorf("wrong result\n%s", diff) 354 } 355 }) 356 } 357 358 } 359 360 func TestModuleSourceRemoteFromRegistry(t *testing.T) { 361 t.Run("both have subdir", func(t *testing.T) { 362 remote := ModuleSourceRemote{ 363 Package: ModulePackage("boop"), 364 Subdir: "foo", 365 } 366 registry := ModuleSourceRegistry{ 367 Subdir: "bar", 368 } 369 gotAddr := remote.FromRegistry(registry) 370 if remote.Subdir != "foo" { 371 t.Errorf("FromRegistry modified the reciever; should be pure function") 372 } 373 if registry.Subdir != "bar" { 374 t.Errorf("FromRegistry modified the given address; should be pure function") 375 } 376 if got, want := gotAddr.Subdir, "foo/bar"; got != want { 377 t.Errorf("wrong resolved subdir\ngot: %s\nwant: %s", got, want) 378 } 379 }) 380 t.Run("only remote has subdir", func(t *testing.T) { 381 remote := ModuleSourceRemote{ 382 Package: ModulePackage("boop"), 383 Subdir: "foo", 384 } 385 registry := ModuleSourceRegistry{ 386 Subdir: "", 387 } 388 gotAddr := remote.FromRegistry(registry) 389 if remote.Subdir != "foo" { 390 t.Errorf("FromRegistry modified the reciever; should be pure function") 391 } 392 if registry.Subdir != "" { 393 t.Errorf("FromRegistry modified the given address; should be pure function") 394 } 395 if got, want := gotAddr.Subdir, "foo"; got != want { 396 t.Errorf("wrong resolved subdir\ngot: %s\nwant: %s", got, want) 397 } 398 }) 399 t.Run("only registry has subdir", func(t *testing.T) { 400 remote := ModuleSourceRemote{ 401 Package: ModulePackage("boop"), 402 Subdir: "", 403 } 404 registry := ModuleSourceRegistry{ 405 Subdir: "bar", 406 } 407 gotAddr := remote.FromRegistry(registry) 408 if remote.Subdir != "" { 409 t.Errorf("FromRegistry modified the reciever; should be pure function") 410 } 411 if registry.Subdir != "bar" { 412 t.Errorf("FromRegistry modified the given address; should be pure function") 413 } 414 if got, want := gotAddr.Subdir, "bar"; got != want { 415 t.Errorf("wrong resolved subdir\ngot: %s\nwant: %s", got, want) 416 } 417 }) 418 } 419 420 func TestParseModuleSourceRemote(t *testing.T) { 421 422 tests := map[string]struct { 423 input string 424 wantString string 425 wantForDisplay string 426 wantErr string 427 }{ 428 "git over HTTPS, URL-style, query parameters": { 429 // Query parameters should be correctly appended after the Package 430 input: `git::https://example.com/code/baz.git?otherthing=blah`, 431 wantString: `git::https://example.com/code/baz.git?otherthing=blah`, 432 wantForDisplay: `git::https://example.com/code/baz.git?otherthing=blah`, 433 }, 434 "git over HTTPS, URL-style, subdir, query parameters": { 435 // Query parameters should be correctly appended after the Package and Subdir 436 input: `git::https://example.com/code/baz.git//bleep/bloop?otherthing=blah`, 437 wantString: `git::https://example.com/code/baz.git//bleep/bloop?otherthing=blah`, 438 wantForDisplay: `git::https://example.com/code/baz.git//bleep/bloop?otherthing=blah`, 439 }, 440 } 441 442 for name, test := range tests { 443 t.Run(name, func(t *testing.T) { 444 remote, err := parseModuleSourceRemote(test.input) 445 446 if test.wantErr != "" { 447 switch { 448 case err == nil: 449 t.Errorf("unexpected success\nwant error: %s", test.wantErr) 450 case err.Error() != test.wantErr: 451 t.Errorf("wrong error messages\ngot: %s\nwant: %s", err.Error(), test.wantErr) 452 } 453 return 454 } 455 456 if err != nil { 457 t.Fatalf("unexpected error: %s", err.Error()) 458 } 459 460 if got, want := remote.String(), test.wantString; got != want { 461 t.Errorf("wrong String() result\ngot: %s\nwant: %s", got, want) 462 } 463 if got, want := remote.ForDisplay(), test.wantForDisplay; got != want { 464 t.Errorf("wrong ForDisplay() result\ngot: %s\nwant: %s", got, want) 465 } 466 }) 467 } 468 } 469 470 func TestParseModuleSourceRegistry(t *testing.T) { 471 // We test parseModuleSourceRegistry alone here, in addition to testing 472 // it indirectly as part of TestParseModuleSource, because general 473 // module parsing unfortunately eats all of the error situations from 474 // registry passing by falling back to trying for a direct remote package 475 // address. 476 477 // Historical note: These test cases were originally derived from the 478 // ones in the old internal/registry/regsrc package that the 479 // ModuleSourceRegistry type is replacing. That package had the notion 480 // of "normalized" addresses as separate from the original user input, 481 // but this new implementation doesn't try to preserve the original 482 // user input at all, and so the main string output is always normalized. 483 // 484 // That package also had some behaviors to turn the namespace, name, and 485 // remote system portions into lowercase, but apparently we didn't 486 // actually make use of that in the end and were preserving the case 487 // the user provided in the input, and so for backward compatibility 488 // we're continuing to do that here, at the expense of now making the 489 // "ForDisplay" output case-preserving where its predecessor in the 490 // old package wasn't. The main OpenTofu Registry at registry.opentofu.org 491 // is itself case-insensitive anyway, so our case-preserving here is 492 // entirely for the benefit of existing third-party registry 493 // implementations that might be case-sensitive, which we must remain 494 // compatible with now. 495 496 tests := map[string]struct { 497 input string 498 wantString string 499 wantForDisplay string 500 wantForProtocol string 501 wantErr string 502 }{ 503 "public registry": { 504 input: `hashicorp/consul/aws`, 505 wantString: `registry.opentofu.org/hashicorp/consul/aws`, 506 wantForDisplay: `hashicorp/consul/aws`, 507 wantForProtocol: `hashicorp/consul/aws`, 508 }, 509 "public registry with subdir": { 510 input: `hashicorp/consul/aws//foo`, 511 wantString: `registry.opentofu.org/hashicorp/consul/aws//foo`, 512 wantForDisplay: `hashicorp/consul/aws//foo`, 513 wantForProtocol: `hashicorp/consul/aws`, 514 }, 515 "public registry using explicit hostname": { 516 input: `registry.opentofu.org/hashicorp/consul/aws`, 517 wantString: `registry.opentofu.org/hashicorp/consul/aws`, 518 wantForDisplay: `hashicorp/consul/aws`, 519 wantForProtocol: `hashicorp/consul/aws`, 520 }, 521 "public registry with mixed case names": { 522 input: `HashiCorp/Consul/aws`, 523 wantString: `registry.opentofu.org/HashiCorp/Consul/aws`, 524 wantForDisplay: `HashiCorp/Consul/aws`, 525 wantForProtocol: `HashiCorp/Consul/aws`, 526 }, 527 "private registry with non-standard port": { 528 input: `Example.com:1234/HashiCorp/Consul/aws`, 529 wantString: `example.com:1234/HashiCorp/Consul/aws`, 530 wantForDisplay: `example.com:1234/HashiCorp/Consul/aws`, 531 wantForProtocol: `HashiCorp/Consul/aws`, 532 }, 533 "private registry with IDN hostname": { 534 input: `Испытание.com/HashiCorp/Consul/aws`, 535 wantString: `испытание.com/HashiCorp/Consul/aws`, 536 wantForDisplay: `испытание.com/HashiCorp/Consul/aws`, 537 wantForProtocol: `HashiCorp/Consul/aws`, 538 }, 539 "private registry with IDN hostname and non-standard port": { 540 input: `Испытание.com:1234/HashiCorp/Consul/aws//Foo`, 541 wantString: `испытание.com:1234/HashiCorp/Consul/aws//Foo`, 542 wantForDisplay: `испытание.com:1234/HashiCorp/Consul/aws//Foo`, 543 wantForProtocol: `HashiCorp/Consul/aws`, 544 }, 545 "invalid hostname": { 546 input: `---.com/HashiCorp/Consul/aws`, 547 wantErr: `invalid module registry hostname "---.com"; internationalized domain names must be given as direct unicode characters, not in punycode`, 548 }, 549 "hostname with only one label": { 550 // This was historically forbidden in our initial implementation, 551 // so we keep it forbidden to avoid newly interpreting such 552 // addresses as registry addresses rather than remote source 553 // addresses. 554 input: `foo/var/baz/qux`, 555 wantErr: `invalid module registry hostname: must contain at least one dot`, 556 }, 557 "invalid target system characters": { 558 input: `foo/var/no-no-no`, 559 wantErr: `invalid target system "no-no-no": must be between one and 64 ASCII letters or digits`, 560 }, 561 "invalid target system length": { 562 input: `foo/var/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah`, 563 wantErr: `invalid target system "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah": must be between one and 64 ASCII letters or digits`, 564 }, 565 "invalid namespace": { 566 input: `boop!/var/baz`, 567 wantErr: `invalid namespace "boop!": must be between one and 64 characters, including ASCII letters, digits, dashes, and underscores, where dashes and underscores may not be the prefix or suffix`, 568 }, 569 "missing part with explicit hostname": { 570 input: `foo.com/var/baz`, 571 wantErr: `source address must have three more components after the hostname: the namespace, the name, and the target system`, 572 }, 573 "errant query string": { 574 input: `foo/var/baz?otherthing`, 575 wantErr: `module registry addresses may not include a query string portion`, 576 }, 577 "github.com": { 578 // We don't allow using github.com like a module registry because 579 // that conflicts with the historically-supported shorthand for 580 // installing directly from GitHub-hosted git repositories. 581 input: `github.com/HashiCorp/Consul/aws`, 582 wantErr: `can't use "github.com" as a module registry host, because it's reserved for installing directly from version control repositories`, 583 }, 584 "bitbucket.org": { 585 // We don't allow using bitbucket.org like a module registry because 586 // that conflicts with the historically-supported shorthand for 587 // installing directly from BitBucket-hosted git repositories. 588 input: `bitbucket.org/HashiCorp/Consul/aws`, 589 wantErr: `can't use "bitbucket.org" as a module registry host, because it's reserved for installing directly from version control repositories`, 590 }, 591 "local path from current dir": { 592 // Can't use a local path when we're specifically trying to parse 593 // a _registry_ source address. 594 input: `./boop`, 595 wantErr: `can't use local directory "./boop" as a module registry address`, 596 }, 597 "local path from parent dir": { 598 // Can't use a local path when we're specifically trying to parse 599 // a _registry_ source address. 600 input: `../boop`, 601 wantErr: `can't use local directory "../boop" as a module registry address`, 602 }, 603 } 604 605 for name, test := range tests { 606 t.Run(name, func(t *testing.T) { 607 addrI, err := ParseModuleSourceRegistry(test.input) 608 609 if test.wantErr != "" { 610 switch { 611 case err == nil: 612 t.Errorf("unexpected success\nwant error: %s", test.wantErr) 613 case err.Error() != test.wantErr: 614 t.Errorf("wrong error messages\ngot: %s\nwant: %s", err.Error(), test.wantErr) 615 } 616 return 617 } 618 619 if err != nil { 620 t.Fatalf("unexpected error: %s", err.Error()) 621 } 622 623 addr, ok := addrI.(ModuleSourceRegistry) 624 if !ok { 625 t.Fatalf("wrong address type %T; want %T", addrI, addr) 626 } 627 628 if got, want := addr.String(), test.wantString; got != want { 629 t.Errorf("wrong String() result\ngot: %s\nwant: %s", got, want) 630 } 631 if got, want := addr.ForDisplay(), test.wantForDisplay; got != want { 632 t.Errorf("wrong ForDisplay() result\ngot: %s\nwant: %s", got, want) 633 } 634 if got, want := addr.Package.ForRegistryProtocol(), test.wantForProtocol; got != want { 635 t.Errorf("wrong ForRegistryProtocol() result\ngot: %s\nwant: %s", got, want) 636 } 637 }) 638 } 639 } 640 641 func testDataAbsolutePath() (absolutePath string, modulePackage string) { 642 absolutePath = "/tmp/foo/example" 643 modulePackage = "file:///tmp/foo/example" 644 if runtime.GOOS == "windows" { 645 absolutePath = "C:\\tmp\\foo\\example" 646 modulePackage = "C:\\tmp\\foo\\example" 647 } 648 return 649 } 650 651 func testDataAbsolutePathSubdir() (absolutePath string, modulePackage string) { 652 absolutePath = "/tmp/foo//example" 653 modulePackage = "file:///tmp/foo" 654 if runtime.GOOS == "windows" { 655 absolutePath = "C:\\tmp\\foo//example" 656 modulePackage = "C:\\tmp\\foo" 657 } 658 return 659 }