github.com/pulumi/terraform@v1.4.0/pkg/addrs/module_source_test.go (about)

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