| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package moduleaddrs | |
| import ( | |
| "testing" | |
| "github.com/google/go-cmp/cmp" | |
| svchost "github.com/hashicorp/terraform-svchost" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| ) | |
| func TestParseModuleSource(t *testing.T) { | |
| tests := map[string]struct { | |
| input string | |
| want addrs.ModuleSource | |
| wantErr string | |
| }{ | |
| // Local paths | |
| "local in subdirectory": { | |
| input: "./child", | |
| want: addrs.ModuleSourceLocal("./child"), | |
| }, | |
| "local in subdirectory non-normalized": { | |
| input: "./nope/../child", | |
| want: addrs.ModuleSourceLocal("./child"), | |
| }, | |
| "local in sibling directory": { | |
| input: "../sibling", | |
| want: addrs.ModuleSourceLocal("../sibling"), | |
| }, | |
| "local in sibling directory non-normalized": { | |
| input: "./nope/../../sibling", | |
| want: addrs.ModuleSourceLocal("../sibling"), | |
| }, | |
| "Windows-style local in subdirectory": { | |
| input: `.\child`, | |
| want: addrs.ModuleSourceLocal("./child"), | |
| }, | |
| "Windows-style local in subdirectory non-normalized": { | |
| input: `.\nope\..\child`, | |
| want: addrs.ModuleSourceLocal("./child"), | |
| }, | |
| "Windows-style local in sibling directory": { | |
| input: `..\sibling`, | |
| want: addrs.ModuleSourceLocal("../sibling"), | |
| }, | |
| "Windows-style local in sibling directory non-normalized": { | |
| input: `.\nope\..\..\sibling`, | |
| want: addrs.ModuleSourceLocal("../sibling"), | |
| }, | |
| "an abominable mix of different slashes": { | |
| input: `./nope\nope/why\./please\don't`, | |
| want: addrs.ModuleSourceLocal("./nope/nope/why/please/don't"), | |
| }, | |
| // Registry addresses | |
| // (NOTE: There is another test function TestParseaddrs.ModuleSourceRegistry | |
| // which tests this situation more exhaustively, so this is just a | |
| // token set of cases to see that we are indeed calling into the | |
| // registry address parser when appropriate.) | |
| "main registry implied": { | |
| input: "hashicorp/subnets/cidr", | |
| want: addrs.ModuleSourceRegistry{ | |
| Package: addrs.ModuleRegistryPackage{ | |
| Host: svchost.Hostname("registry.terraform.io"), | |
| Namespace: "hashicorp", | |
| Name: "subnets", | |
| TargetSystem: "cidr", | |
| }, | |
| Subdir: "", | |
| }, | |
| }, | |
| "main registry implied, subdir": { | |
| input: "hashicorp/subnets/cidr//examples/foo", | |
| want: addrs.ModuleSourceRegistry{ | |
| Package: addrs.ModuleRegistryPackage{ | |
| Host: svchost.Hostname("registry.terraform.io"), | |
| Namespace: "hashicorp", | |
| Name: "subnets", | |
| TargetSystem: "cidr", | |
| }, | |
| Subdir: "examples/foo", | |
| }, | |
| }, | |
| "main registry implied, escaping subdir": { | |
| input: "hashicorp/subnets/cidr//../nope", | |
| // NOTE: This error is actually being caught by the _remote package_ | |
| // address parser, because any registry parsing failure falls back | |
| // to that but both of them have the same subdir validation. This | |
| // case is here to make sure that stays true, so we keep reporting | |
| // a suitable error when the user writes a registry-looking thing. | |
| wantErr: `subdirectory path "../nope" leads outside of the module package`, | |
| }, | |
| "custom registry": { | |
| input: "example.com/awesomecorp/network/happycloud", | |
| want: addrs.ModuleSourceRegistry{ | |
| Package: addrs.ModuleRegistryPackage{ | |
| Host: svchost.Hostname("example.com"), | |
| Namespace: "awesomecorp", | |
| Name: "network", | |
| TargetSystem: "happycloud", | |
| }, | |
| Subdir: "", | |
| }, | |
| }, | |
| "custom registry, subdir": { | |
| input: "example.com/awesomecorp/network/happycloud//examples/foo", | |
| want: addrs.ModuleSourceRegistry{ | |
| Package: addrs.ModuleRegistryPackage{ | |
| Host: svchost.Hostname("example.com"), | |
| Namespace: "awesomecorp", | |
| Name: "network", | |
| TargetSystem: "happycloud", | |
| }, | |
| Subdir: "examples/foo", | |
| }, | |
| }, | |
| // Remote package addresses | |
| "github.com shorthand": { | |
| input: "github.com/hashicorp/terraform-cidr-subnets", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("git::https://github.com/hashicorp/terraform-cidr-subnets.git"), | |
| }, | |
| }, | |
| "github.com shorthand, subdir": { | |
| input: "github.com/hashicorp/terraform-cidr-subnets//example/foo", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("git::https://github.com/hashicorp/terraform-cidr-subnets.git"), | |
| Subdir: "example/foo", | |
| }, | |
| }, | |
| "git protocol, URL-style": { | |
| input: "git://example.com/code/baz.git", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("git://example.com/code/baz.git"), | |
| }, | |
| }, | |
| "git protocol, URL-style, subdir": { | |
| input: "git://example.com/code/baz.git//bleep/bloop", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("git://example.com/code/baz.git"), | |
| Subdir: "bleep/bloop", | |
| }, | |
| }, | |
| "git over HTTPS, URL-style": { | |
| input: "git::https://example.com/code/baz.git", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("git::https://example.com/code/baz.git"), | |
| }, | |
| }, | |
| "git over HTTPS, URL-style, subdir": { | |
| input: "git::https://example.com/code/baz.git//bleep/bloop", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("git::https://example.com/code/baz.git"), | |
| Subdir: "bleep/bloop", | |
| }, | |
| }, | |
| "git over HTTPS, URL-style, subdir, query parameters": { | |
| input: "git::https://example.com/code/baz.git//bleep/bloop?otherthing=blah", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("git::https://example.com/code/baz.git?otherthing=blah"), | |
| Subdir: "bleep/bloop", | |
| }, | |
| }, | |
| "git over SSH, URL-style": { | |
| input: "git::ssh://git@example.com/code/baz.git", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("git::ssh://git@example.com/code/baz.git"), | |
| }, | |
| }, | |
| "git over SSH, URL-style, subdir": { | |
| input: "git::ssh://git@example.com/code/baz.git//bleep/bloop", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("git::ssh://git@example.com/code/baz.git"), | |
| Subdir: "bleep/bloop", | |
| }, | |
| }, | |
| "git over SSH, scp-style": { | |
| input: "git::git@example.com:code/baz.git", | |
| want: addrs.ModuleSourceRemote{ | |
| // Normalized to URL-style | |
| Package: addrs.ModulePackage("git::ssh://git@example.com/code/baz.git"), | |
| }, | |
| }, | |
| "git over SSH, scp-style, subdir": { | |
| input: "git::git@example.com:code/baz.git//bleep/bloop", | |
| want: addrs.ModuleSourceRemote{ | |
| // Normalized to URL-style | |
| Package: addrs.ModulePackage("git::ssh://git@example.com/code/baz.git"), | |
| Subdir: "bleep/bloop", | |
| }, | |
| }, | |
| // NOTE: We intentionally don't test the bitbucket.org shorthands | |
| // here, because that detector makes direct HTTP tequests to the | |
| // Bitbucket API and thus isn't appropriate for unit testing. | |
| "Google Cloud Storage bucket implied, path prefix": { | |
| input: "www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE"), | |
| }, | |
| }, | |
| "Google Cloud Storage bucket, path prefix": { | |
| input: "gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE"), | |
| }, | |
| }, | |
| "Google Cloud Storage bucket implied, archive object": { | |
| input: "www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip"), | |
| }, | |
| }, | |
| "Google Cloud Storage bucket, archive object": { | |
| input: "gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip"), | |
| }, | |
| }, | |
| "Amazon S3 bucket implied, archive object": { | |
| input: "s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip"), | |
| }, | |
| }, | |
| "Amazon S3 bucket, archive object": { | |
| input: "s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip"), | |
| }, | |
| }, | |
| "HTTP URL": { | |
| input: "http://example.com/module", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("http://example.com/module"), | |
| }, | |
| }, | |
| "HTTPS URL": { | |
| input: "https://example.com/module", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("https://example.com/module"), | |
| }, | |
| }, | |
| "HTTPS URL, archive file": { | |
| input: "https://example.com/module.zip", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("https://example.com/module.zip"), | |
| }, | |
| }, | |
| "HTTPS URL, forced archive file": { | |
| input: "https://example.com/module?archive=tar", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("https://example.com/module?archive=tar"), | |
| }, | |
| }, | |
| "HTTPS URL, forced archive file and checksum": { | |
| input: "https://example.com/module?archive=tar&checksum=blah", | |
| want: addrs.ModuleSourceRemote{ | |
| // The query string only actually gets processed when we finally | |
| // do the get, so "checksum=blah" is accepted as valid up | |
| // at this parsing layer. | |
| Package: addrs.ModulePackage("https://example.com/module?archive=tar&checksum=blah"), | |
| }, | |
| }, | |
| "absolute filesystem path": { | |
| // Although a local directory isn't really "remote", we do | |
| // treat it as such because we still need to do all of the same | |
| // high-level steps to work with these, even though "downloading" | |
| // is replaced by a deep filesystem copy instead. | |
| input: "/tmp/foo/example", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("file:///tmp/foo/example"), | |
| }, | |
| }, | |
| "absolute filesystem path, subdir": { | |
| // This is a funny situation where the user wants to use a | |
| // directory elsewhere on their system as a package containing | |
| // multiple modules, but the entry point is not at the root | |
| // of that subtree, and so they can use the usual subdir | |
| // syntax to move the package root higher in the real filesystem. | |
| input: "/tmp/foo//example", | |
| want: addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("file:///tmp/foo"), | |
| Subdir: "example", | |
| }, | |
| }, | |
| "subdir escaping out of package": { | |
| // This is general logic for all subdir regardless of installation | |
| // protocol, but we're using a filesystem path here just as an | |
| // easy placeholder/ | |
| input: "/tmp/foo//example/../../invalid", | |
| wantErr: `subdirectory path "../invalid" leads outside of the module package`, | |
| }, | |
| "relative path without the needed prefix": { | |
| input: "boop/bloop", | |
| // For this case we return a generic error message from the addrs | |
| // layer, but using a specialized error type which our module | |
| // installer checks for and produces an extra hint for users who | |
| // were intending to write a local path which then got | |
| // misinterpreted as a remote source due to the missing prefix. | |
| // However, the main message is generic here because this is really | |
| // just a general "this string doesn't match any of our source | |
| // address patterns" situation, not _necessarily_ about relative | |
| // local paths. | |
| wantErr: `Terraform cannot detect a supported external module source type for boop/bloop`, | |
| }, | |
| "go-getter will accept all sorts of garbage": { | |
| input: "dfgdfgsd:dgfhdfghdfghdfg/dfghdfghdfg", | |
| want: addrs.ModuleSourceRemote{ | |
| // Unfortunately go-getter doesn't actually reject a totally | |
| // invalid address like this until getting time, as long as | |
| // it looks somewhat like a URL. | |
| Package: addrs.ModulePackage("dfgdfgsd:dgfhdfghdfghdfg/dfghdfghdfg"), | |
| }, | |
| }, | |
| } | |
| for name, test := range tests { | |
| t.Run(name, func(t *testing.T) { | |
| addr, err := ParseModuleSource(test.input) | |
| if test.wantErr != "" { | |
| switch { | |
| case err == nil: | |
| t.Errorf("unexpected success\nwant error: %s", test.wantErr) | |
| case err.Error() != test.wantErr: | |
| t.Errorf("wrong error messages\ngot: %s\nwant: %s", err.Error(), test.wantErr) | |
| } | |
| return | |
| } | |
| if err != nil { | |
| t.Fatalf("unexpected error: %s", err.Error()) | |
| } | |
| if diff := cmp.Diff(addr, test.want); diff != "" { | |
| t.Errorf("wrong result\n%s", diff) | |
| } | |
| }) | |
| } | |
| } | |
| func TestModuleSourceRemoteFromRegistry(t *testing.T) { | |
| t.Run("both have subdir", func(t *testing.T) { | |
| remote := addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("boop"), | |
| Subdir: "foo", | |
| } | |
| registry := addrs.ModuleSourceRegistry{ | |
| Subdir: "bar", | |
| } | |
| gotAddr := remote.FromRegistry(registry) | |
| if remote.Subdir != "foo" { | |
| t.Errorf("FromRegistry modified the reciever; should be pure function") | |
| } | |
| if registry.Subdir != "bar" { | |
| t.Errorf("FromRegistry modified the given address; should be pure function") | |
| } | |
| if got, want := gotAddr.Subdir, "foo/bar"; got != want { | |
| t.Errorf("wrong resolved subdir\ngot: %s\nwant: %s", got, want) | |
| } | |
| }) | |
| t.Run("only remote has subdir", func(t *testing.T) { | |
| remote := addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("boop"), | |
| Subdir: "foo", | |
| } | |
| registry := addrs.ModuleSourceRegistry{ | |
| Subdir: "", | |
| } | |
| gotAddr := remote.FromRegistry(registry) | |
| if remote.Subdir != "foo" { | |
| t.Errorf("FromRegistry modified the reciever; should be pure function") | |
| } | |
| if registry.Subdir != "" { | |
| t.Errorf("FromRegistry modified the given address; should be pure function") | |
| } | |
| if got, want := gotAddr.Subdir, "foo"; got != want { | |
| t.Errorf("wrong resolved subdir\ngot: %s\nwant: %s", got, want) | |
| } | |
| }) | |
| t.Run("only registry has subdir", func(t *testing.T) { | |
| remote := addrs.ModuleSourceRemote{ | |
| Package: addrs.ModulePackage("boop"), | |
| Subdir: "", | |
| } | |
| registry := addrs.ModuleSourceRegistry{ | |
| Subdir: "bar", | |
| } | |
| gotAddr := remote.FromRegistry(registry) | |
| if remote.Subdir != "" { | |
| t.Errorf("FromRegistry modified the reciever; should be pure function") | |
| } | |
| if registry.Subdir != "bar" { | |
| t.Errorf("FromRegistry modified the given address; should be pure function") | |
| } | |
| if got, want := gotAddr.Subdir, "bar"; got != want { | |
| t.Errorf("wrong resolved subdir\ngot: %s\nwant: %s", got, want) | |
| } | |
| }) | |
| } | |
| func TestParseModuleSourceRemote(t *testing.T) { | |
| tests := map[string]struct { | |
| input string | |
| wantString string | |
| wantForDisplay string | |
| wantErr string | |
| }{ | |
| "git over HTTPS, URL-style, query parameters": { | |
| // Query parameters should be correctly appended after the Package | |
| input: `git::https://example.com/code/baz.git?otherthing=blah`, | |
| wantString: `git::https://example.com/code/baz.git?otherthing=blah`, | |
| wantForDisplay: `git::https://example.com/code/baz.git?otherthing=blah`, | |
| }, | |
| "git over HTTPS, URL-style, subdir, query parameters": { | |
| // Query parameters should be correctly appended after the Package and Subdir | |
| input: `git::https://example.com/code/baz.git//bleep/bloop?otherthing=blah`, | |
| wantString: `git::https://example.com/code/baz.git//bleep/bloop?otherthing=blah`, | |
| wantForDisplay: `git::https://example.com/code/baz.git//bleep/bloop?otherthing=blah`, | |
| }, | |
| } | |
| for name, test := range tests { | |
| t.Run(name, func(t *testing.T) { | |
| remote, err := parseModuleSourceRemote(test.input) | |
| if test.wantErr != "" { | |
| switch { | |
| case err == nil: | |
| t.Errorf("unexpected success\nwant error: %s", test.wantErr) | |
| case err.Error() != test.wantErr: | |
| t.Errorf("wrong error messages\ngot: %s\nwant: %s", err.Error(), test.wantErr) | |
| } | |
| return | |
| } | |
| if err != nil { | |
| t.Fatalf("unexpected error: %s", err.Error()) | |
| } | |
| if got, want := remote.String(), test.wantString; got != want { | |
| t.Errorf("wrong String() result\ngot: %s\nwant: %s", got, want) | |
| } | |
| if got, want := remote.ForDisplay(), test.wantForDisplay; got != want { | |
| t.Errorf("wrong ForDisplay() result\ngot: %s\nwant: %s", got, want) | |
| } | |
| }) | |
| } | |
| } | |
| func TestParseModuleSourceRegistry(t *testing.T) { | |
| // We test ParseModuleSourceRegistry alone here, in addition to testing | |
| // it indirectly as part of TestParseaddrs.ModuleSource, because general | |
| // module parsing unfortunately eats all of the error situations from | |
| // registry passing by falling back to trying for a direct remote package | |
| // address. | |
| // Historical note: These test cases were originally derived from the | |
| // ones in the old internal/registry/regsrc package that the | |
| // addrs.ModuleSourceRegistry type is replacing. That package had the notion | |
| // of "normalized" addresses as separate from the original user input, | |
| // but this new implementation doesn't try to preserve the original | |
| // user input at all, and so the main string output is always normalized. | |
| // | |
| // That package also had some behaviors to turn the namespace, name, and | |
| // remote system portions into lowercase, but apparently we didn't | |
| // actually make use of that in the end and were preserving the case | |
| // the user provided in the input, and so for backward compatibility | |
| // we're continuing to do that here, at the expense of now making the | |
| // "ForDisplay" output case-preserving where its predecessor in the | |
| // old package wasn't. The main Terraform Registry at registry.terraform.io | |
| // is itself case-insensitive anyway, so our case-preserving here is | |
| // entirely for the benefit of existing third-party registry | |
| // implementations that might be case-sensitive, which we must remain | |
| // compatible with now. | |
| tests := map[string]struct { | |
| input string | |
| wantString string | |
| wantForDisplay string | |
| wantForProtocol string | |
| wantErr string | |
| }{ | |
| "public registry": { | |
| input: `hashicorp/consul/aws`, | |
| wantString: `registry.terraform.io/hashicorp/consul/aws`, | |
| wantForDisplay: `hashicorp/consul/aws`, | |
| wantForProtocol: `hashicorp/consul/aws`, | |
| }, | |
| "public registry with subdir": { | |
| input: `hashicorp/consul/aws//foo`, | |
| wantString: `registry.terraform.io/hashicorp/consul/aws//foo`, | |
| wantForDisplay: `hashicorp/consul/aws//foo`, | |
| wantForProtocol: `hashicorp/consul/aws`, | |
| }, | |
| "public registry using explicit hostname": { | |
| input: `registry.terraform.io/hashicorp/consul/aws`, | |
| wantString: `registry.terraform.io/hashicorp/consul/aws`, | |
| wantForDisplay: `hashicorp/consul/aws`, | |
| wantForProtocol: `hashicorp/consul/aws`, | |
| }, | |
| "public registry with mixed case names": { | |
| input: `HashiCorp/Consul/aws`, | |
| wantString: `registry.terraform.io/HashiCorp/Consul/aws`, | |
| wantForDisplay: `HashiCorp/Consul/aws`, | |
| wantForProtocol: `HashiCorp/Consul/aws`, | |
| }, | |
| "private registry with non-standard port": { | |
| input: `Example.com:1234/HashiCorp/Consul/aws`, | |
| wantString: `example.com:1234/HashiCorp/Consul/aws`, | |
| wantForDisplay: `example.com:1234/HashiCorp/Consul/aws`, | |
| wantForProtocol: `HashiCorp/Consul/aws`, | |
| }, | |
| "private registry with IDN hostname": { | |
| input: `Испытание.com/HashiCorp/Consul/aws`, | |
| wantString: `испытание.com/HashiCorp/Consul/aws`, | |
| wantForDisplay: `испытание.com/HashiCorp/Consul/aws`, | |
| wantForProtocol: `HashiCorp/Consul/aws`, | |
| }, | |
| "private registry with IDN hostname and non-standard port": { | |
| input: `Испытание.com:1234/HashiCorp/Consul/aws//Foo`, | |
| wantString: `испытание.com:1234/HashiCorp/Consul/aws//Foo`, | |
| wantForDisplay: `испытание.com:1234/HashiCorp/Consul/aws//Foo`, | |
| wantForProtocol: `HashiCorp/Consul/aws`, | |
| }, | |
| "invalid hostname": { | |
| input: `---.com/HashiCorp/Consul/aws`, | |
| wantErr: `invalid module registry hostname "---.com"; internationalized domain names must be given as direct unicode characters, not in punycode`, | |
| }, | |
| "hostname with only one label": { | |
| // This was historically forbidden in our initial implementation, | |
| // so we keep it forbidden to avoid newly interpreting such | |
| // addresses as registry addresses rather than remote source | |
| // addresses. | |
| input: `foo/var/baz/qux`, | |
| wantErr: `invalid module registry hostname: must contain at least one dot`, | |
| }, | |
| "invalid target system characters": { | |
| input: `foo/var/no-no-no`, | |
| wantErr: `invalid target system "no-no-no": must be between one and 64 ASCII letters or digits`, | |
| }, | |
| "invalid target system length": { | |
| input: `foo/var/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah`, | |
| wantErr: `invalid target system "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah": must be between one and 64 ASCII letters or digits`, | |
| }, | |
| "invalid namespace": { | |
| input: `boop!/var/baz`, | |
| 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`, | |
| }, | |
| "missing part with explicit hostname": { | |
| input: `foo.com/var/baz`, | |
| wantErr: `source address must have three more components after the hostname: the namespace, the name, and the target system`, | |
| }, | |
| "errant query string": { | |
| input: `foo/var/baz?otherthing`, | |
| wantErr: `module registry addresses may not include a query string portion`, | |
| }, | |
| "github.com": { | |
| // We don't allow using github.com like a module registry because | |
| // that conflicts with the historically-supported shorthand for | |
| // installing directly from GitHub-hosted git repositories. | |
| input: `github.com/HashiCorp/Consul/aws`, | |
| wantErr: `can't use "github.com" as a module registry host, because it's reserved for installing directly from version control repositories`, | |
| }, | |
| "bitbucket.org": { | |
| // We don't allow using bitbucket.org like a module registry because | |
| // that conflicts with the historically-supported shorthand for | |
| // installing directly from BitBucket-hosted git repositories. | |
| input: `bitbucket.org/HashiCorp/Consul/aws`, | |
| wantErr: `can't use "bitbucket.org" as a module registry host, because it's reserved for installing directly from version control repositories`, | |
| }, | |
| "local path from current dir": { | |
| // Can't use a local path when we're specifically trying to parse | |
| // a _registry_ source address. | |
| input: `./boop`, | |
| wantErr: `can't use local directory "./boop" as a module registry address`, | |
| }, | |
| "local path from parent dir": { | |
| // Can't use a local path when we're specifically trying to parse | |
| // a _registry_ source address. | |
| input: `../boop`, | |
| wantErr: `can't use local directory "../boop" as a module registry address`, | |
| }, | |
| } | |
| for name, test := range tests { | |
| t.Run(name, func(t *testing.T) { | |
| addrI, err := ParseModuleSourceRegistry(test.input) | |
| if test.wantErr != "" { | |
| switch { | |
| case err == nil: | |
| t.Errorf("unexpected success\nwant error: %s", test.wantErr) | |
| case err.Error() != test.wantErr: | |
| t.Errorf("wrong error messages\ngot: %s\nwant: %s", err.Error(), test.wantErr) | |
| } | |
| return | |
| } | |
| if err != nil { | |
| t.Fatalf("unexpected error: %s", err.Error()) | |
| } | |
| addr, ok := addrI.(addrs.ModuleSourceRegistry) | |
| if !ok { | |
| t.Fatalf("wrong address type %T; want %T", addrI, addr) | |
| } | |
| if got, want := addr.String(), test.wantString; got != want { | |
| t.Errorf("wrong String() result\ngot: %s\nwant: %s", got, want) | |
| } | |
| if got, want := addr.ForDisplay(), test.wantForDisplay; got != want { | |
| t.Errorf("wrong ForDisplay() result\ngot: %s\nwant: %s", got, want) | |
| } | |
| if got, want := addr.Package.ForRegistryProtocol(), test.wantForProtocol; got != want { | |
| t.Errorf("wrong ForRegistryProtocol() result\ngot: %s\nwant: %s", got, want) | |
| } | |
| }) | |
| } | |
| } |