github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/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  				PackageAddr: 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  				PackageAddr: 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  				PackageAddr: 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  				PackageAddr: 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  				PackageAddr: 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  				PackageAddr: 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  				PackageAddr: 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  				PackageAddr: 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  				PackageAddr: 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  				PackageAddr: ModulePackage("git::https://example.com/code/baz.git"),
   154  				Subdir:      "bleep/bloop",
   155  			},
   156  		},
   157  		"git over SSH, URL-style": {
   158  			input: "git::ssh://git@example.com/code/baz.git",
   159  			want: ModuleSourceRemote{
   160  				PackageAddr: ModulePackage("git::ssh://git@example.com/code/baz.git"),
   161  			},
   162  		},
   163  		"git over SSH, URL-style, subdir": {
   164  			input: "git::ssh://git@example.com/code/baz.git//bleep/bloop",
   165  			want: ModuleSourceRemote{
   166  				PackageAddr: ModulePackage("git::ssh://git@example.com/code/baz.git"),
   167  				Subdir:      "bleep/bloop",
   168  			},
   169  		},
   170  		"git over SSH, scp-style": {
   171  			input: "git::git@example.com:code/baz.git",
   172  			want: ModuleSourceRemote{
   173  				// Normalized to URL-style
   174  				PackageAddr: ModulePackage("git::ssh://git@example.com/code/baz.git"),
   175  			},
   176  		},
   177  		"git over SSH, scp-style, subdir": {
   178  			input: "git::git@example.com:code/baz.git//bleep/bloop",
   179  			want: ModuleSourceRemote{
   180  				// Normalized to URL-style
   181  				PackageAddr: ModulePackage("git::ssh://git@example.com/code/baz.git"),
   182  				Subdir:      "bleep/bloop",
   183  			},
   184  		},
   185  
   186  		// NOTE: We intentionally don't test the bitbucket.org shorthands
   187  		// here, because that detector makes direct HTTP tequests to the
   188  		// Bitbucket API and thus isn't appropriate for unit testing.
   189  
   190  		"Google Cloud Storage bucket implied, path prefix": {
   191  			input: "www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE",
   192  			want: ModuleSourceRemote{
   193  				PackageAddr: ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE"),
   194  			},
   195  		},
   196  		"Google Cloud Storage bucket, path prefix": {
   197  			input: "gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE",
   198  			want: ModuleSourceRemote{
   199  				PackageAddr: ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE"),
   200  			},
   201  		},
   202  		"Google Cloud Storage bucket implied, archive object": {
   203  			input: "www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip",
   204  			want: ModuleSourceRemote{
   205  				PackageAddr: ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip"),
   206  			},
   207  		},
   208  		"Google Cloud Storage bucket, archive object": {
   209  			input: "gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip",
   210  			want: ModuleSourceRemote{
   211  				PackageAddr: ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip"),
   212  			},
   213  		},
   214  
   215  		"Amazon S3 bucket implied, archive object": {
   216  			input: "s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip",
   217  			want: ModuleSourceRemote{
   218  				PackageAddr: ModulePackage("s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip"),
   219  			},
   220  		},
   221  		"Amazon S3 bucket, archive object": {
   222  			input: "s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip",
   223  			want: ModuleSourceRemote{
   224  				PackageAddr: ModulePackage("s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip"),
   225  			},
   226  		},
   227  
   228  		"HTTP URL": {
   229  			input: "http://example.com/module",
   230  			want: ModuleSourceRemote{
   231  				PackageAddr: ModulePackage("http://example.com/module"),
   232  			},
   233  		},
   234  		"HTTPS URL": {
   235  			input: "https://example.com/module",
   236  			want: ModuleSourceRemote{
   237  				PackageAddr: ModulePackage("https://example.com/module"),
   238  			},
   239  		},
   240  		"HTTPS URL, archive file": {
   241  			input: "https://example.com/module.zip",
   242  			want: ModuleSourceRemote{
   243  				PackageAddr: ModulePackage("https://example.com/module.zip"),
   244  			},
   245  		},
   246  		"HTTPS URL, forced archive file": {
   247  			input: "https://example.com/module?archive=tar",
   248  			want: ModuleSourceRemote{
   249  				PackageAddr: ModulePackage("https://example.com/module?archive=tar"),
   250  			},
   251  		},
   252  		"HTTPS URL, forced archive file and checksum": {
   253  			input: "https://example.com/module?archive=tar&checksum=blah",
   254  			want: ModuleSourceRemote{
   255  				// The query string only actually gets processed when we finally
   256  				// do the get, so "checksum=blah" is accepted as valid up
   257  				// at this parsing layer.
   258  				PackageAddr: ModulePackage("https://example.com/module?archive=tar&checksum=blah"),
   259  			},
   260  		},
   261  
   262  		"absolute filesystem path": {
   263  			// Although a local directory isn't really "remote", we do
   264  			// treat it as such because we still need to do all of the same
   265  			// high-level steps to work with these, even though "downloading"
   266  			// is replaced by a deep filesystem copy instead.
   267  			input: "/tmp/foo/example",
   268  			want: ModuleSourceRemote{
   269  				PackageAddr: ModulePackage("file:///tmp/foo/example"),
   270  			},
   271  		},
   272  		"absolute filesystem path, subdir": {
   273  			// This is a funny situation where the user wants to use a
   274  			// directory elsewhere on their system as a package containing
   275  			// multiple modules, but the entry point is not at the root
   276  			// of that subtree, and so they can use the usual subdir
   277  			// syntax to move the package root higher in the real filesystem.
   278  			input: "/tmp/foo//example",
   279  			want: ModuleSourceRemote{
   280  				PackageAddr: ModulePackage("file:///tmp/foo"),
   281  				Subdir:      "example",
   282  			},
   283  		},
   284  
   285  		"subdir escaping out of package": {
   286  			// This is general logic for all subdir regardless of installation
   287  			// protocol, but we're using a filesystem path here just as an
   288  			// easy placeholder/
   289  			input:   "/tmp/foo//example/../../invalid",
   290  			wantErr: `subdirectory path "../invalid" leads outside of the module package`,
   291  		},
   292  
   293  		"relative path without the needed prefix": {
   294  			input: "boop/bloop",
   295  			// For this case we return a generic error message from the addrs
   296  			// layer, but using a specialized error type which our module
   297  			// installer checks for and produces an extra hint for users who
   298  			// were intending to write a local path which then got
   299  			// misinterpreted as a remote source due to the missing prefix.
   300  			// However, the main message is generic here because this is really
   301  			// just a general "this string doesn't match any of our source
   302  			// address patterns" situation, not _necessarily_ about relative
   303  			// local paths.
   304  			wantErr: `Terraform cannot detect a supported external module source type for boop/bloop`,
   305  		},
   306  
   307  		"go-getter will accept all sorts of garbage": {
   308  			input: "dfgdfgsd:dgfhdfghdfghdfg/dfghdfghdfg",
   309  			want: ModuleSourceRemote{
   310  				// Unfortunately go-getter doesn't actually reject a totally
   311  				// invalid address like this until getting time, as long as
   312  				// it looks somewhat like a URL.
   313  				PackageAddr: ModulePackage("dfgdfgsd:dgfhdfghdfghdfg/dfghdfghdfg"),
   314  			},
   315  		},
   316  	}
   317  
   318  	for name, test := range tests {
   319  		t.Run(name, func(t *testing.T) {
   320  			addr, err := ParseModuleSource(test.input)
   321  
   322  			if test.wantErr != "" {
   323  				switch {
   324  				case err == nil:
   325  					t.Errorf("unexpected success\nwant error: %s", test.wantErr)
   326  				case err.Error() != test.wantErr:
   327  					t.Errorf("wrong error messages\ngot:  %s\nwant: %s", err.Error(), test.wantErr)
   328  				}
   329  				return
   330  			}
   331  
   332  			if err != nil {
   333  				t.Fatalf("unexpected error: %s", err.Error())
   334  			}
   335  
   336  			if diff := cmp.Diff(addr, test.want); diff != "" {
   337  				t.Errorf("wrong result\n%s", diff)
   338  			}
   339  		})
   340  	}
   341  
   342  }
   343  
   344  func TestModuleSourceRemoteFromRegistry(t *testing.T) {
   345  	t.Run("both have subdir", func(t *testing.T) {
   346  		remote := ModuleSourceRemote{
   347  			PackageAddr: ModulePackage("boop"),
   348  			Subdir:      "foo",
   349  		}
   350  		registry := ModuleSourceRegistry{
   351  			Subdir: "bar",
   352  		}
   353  		gotAddr := remote.FromRegistry(registry)
   354  		if remote.Subdir != "foo" {
   355  			t.Errorf("FromRegistry modified the reciever; should be pure function")
   356  		}
   357  		if registry.Subdir != "bar" {
   358  			t.Errorf("FromRegistry modified the given address; should be pure function")
   359  		}
   360  		if got, want := gotAddr.Subdir, "foo/bar"; got != want {
   361  			t.Errorf("wrong resolved subdir\ngot:  %s\nwant: %s", got, want)
   362  		}
   363  	})
   364  	t.Run("only remote has subdir", func(t *testing.T) {
   365  		remote := ModuleSourceRemote{
   366  			PackageAddr: ModulePackage("boop"),
   367  			Subdir:      "foo",
   368  		}
   369  		registry := ModuleSourceRegistry{
   370  			Subdir: "",
   371  		}
   372  		gotAddr := remote.FromRegistry(registry)
   373  		if remote.Subdir != "foo" {
   374  			t.Errorf("FromRegistry modified the reciever; should be pure function")
   375  		}
   376  		if registry.Subdir != "" {
   377  			t.Errorf("FromRegistry modified the given address; should be pure function")
   378  		}
   379  		if got, want := gotAddr.Subdir, "foo"; got != want {
   380  			t.Errorf("wrong resolved subdir\ngot:  %s\nwant: %s", got, want)
   381  		}
   382  	})
   383  	t.Run("only registry has subdir", func(t *testing.T) {
   384  		remote := ModuleSourceRemote{
   385  			PackageAddr: ModulePackage("boop"),
   386  			Subdir:      "",
   387  		}
   388  		registry := ModuleSourceRegistry{
   389  			Subdir: "bar",
   390  		}
   391  		gotAddr := remote.FromRegistry(registry)
   392  		if remote.Subdir != "" {
   393  			t.Errorf("FromRegistry modified the reciever; should be pure function")
   394  		}
   395  		if registry.Subdir != "bar" {
   396  			t.Errorf("FromRegistry modified the given address; should be pure function")
   397  		}
   398  		if got, want := gotAddr.Subdir, "bar"; got != want {
   399  			t.Errorf("wrong resolved subdir\ngot:  %s\nwant: %s", got, want)
   400  		}
   401  	})
   402  }
   403  
   404  func TestParseModuleSourceRegistry(t *testing.T) {
   405  	// We test parseModuleSourceRegistry alone here, in addition to testing
   406  	// it indirectly as part of TestParseModuleSource, because general
   407  	// module parsing unfortunately eats all of the error situations from
   408  	// registry passing by falling back to trying for a direct remote package
   409  	// address.
   410  
   411  	// Historical note: These test cases were originally derived from the
   412  	// ones in the old internal/registry/regsrc package that the
   413  	// ModuleSourceRegistry type is replacing. That package had the notion
   414  	// of "normalized" addresses as separate from the original user input,
   415  	// but this new implementation doesn't try to preserve the original
   416  	// user input at all, and so the main string output is always normalized.
   417  	//
   418  	// That package also had some behaviors to turn the namespace, name, and
   419  	// remote system portions into lowercase, but apparently we didn't
   420  	// actually make use of that in the end and were preserving the case
   421  	// the user provided in the input, and so for backward compatibility
   422  	// we're continuing to do that here, at the expense of now making the
   423  	// "ForDisplay" output case-preserving where its predecessor in the
   424  	// old package wasn't. The main Terraform Registry at registry.terraform.io
   425  	// is itself case-insensitive anyway, so our case-preserving here is
   426  	// entirely for the benefit of existing third-party registry
   427  	// implementations that might be case-sensitive, which we must remain
   428  	// compatible with now.
   429  
   430  	tests := map[string]struct {
   431  		input           string
   432  		wantString      string
   433  		wantForDisplay  string
   434  		wantForProtocol string
   435  		wantErr         string
   436  	}{
   437  		"public registry": {
   438  			input:           `hashicorp/consul/aws`,
   439  			wantString:      `registry.terraform.io/hashicorp/consul/aws`,
   440  			wantForDisplay:  `hashicorp/consul/aws`,
   441  			wantForProtocol: `hashicorp/consul/aws`,
   442  		},
   443  		"public registry with subdir": {
   444  			input:           `hashicorp/consul/aws//foo`,
   445  			wantString:      `registry.terraform.io/hashicorp/consul/aws//foo`,
   446  			wantForDisplay:  `hashicorp/consul/aws//foo`,
   447  			wantForProtocol: `hashicorp/consul/aws`,
   448  		},
   449  		"public registry using explicit hostname": {
   450  			input:           `registry.terraform.io/hashicorp/consul/aws`,
   451  			wantString:      `registry.terraform.io/hashicorp/consul/aws`,
   452  			wantForDisplay:  `hashicorp/consul/aws`,
   453  			wantForProtocol: `hashicorp/consul/aws`,
   454  		},
   455  		"public registry with mixed case names": {
   456  			input:           `HashiCorp/Consul/aws`,
   457  			wantString:      `registry.terraform.io/HashiCorp/Consul/aws`,
   458  			wantForDisplay:  `HashiCorp/Consul/aws`,
   459  			wantForProtocol: `HashiCorp/Consul/aws`,
   460  		},
   461  		"private registry with non-standard port": {
   462  			input:           `Example.com:1234/HashiCorp/Consul/aws`,
   463  			wantString:      `example.com:1234/HashiCorp/Consul/aws`,
   464  			wantForDisplay:  `example.com:1234/HashiCorp/Consul/aws`,
   465  			wantForProtocol: `HashiCorp/Consul/aws`,
   466  		},
   467  		"private registry with IDN hostname": {
   468  			input:           `Испытание.com/HashiCorp/Consul/aws`,
   469  			wantString:      `испытание.com/HashiCorp/Consul/aws`,
   470  			wantForDisplay:  `испытание.com/HashiCorp/Consul/aws`,
   471  			wantForProtocol: `HashiCorp/Consul/aws`,
   472  		},
   473  		"private registry with IDN hostname and non-standard port": {
   474  			input:           `Испытание.com:1234/HashiCorp/Consul/aws//Foo`,
   475  			wantString:      `испытание.com:1234/HashiCorp/Consul/aws//Foo`,
   476  			wantForDisplay:  `испытание.com:1234/HashiCorp/Consul/aws//Foo`,
   477  			wantForProtocol: `HashiCorp/Consul/aws`,
   478  		},
   479  		"invalid hostname": {
   480  			input:   `---.com/HashiCorp/Consul/aws`,
   481  			wantErr: `invalid module registry hostname "---.com"; internationalized domain names must be given as direct unicode characters, not in punycode`,
   482  		},
   483  		"hostname with only one label": {
   484  			// This was historically forbidden in our initial implementation,
   485  			// so we keep it forbidden to avoid newly interpreting such
   486  			// addresses as registry addresses rather than remote source
   487  			// addresses.
   488  			input:   `foo/var/baz/qux`,
   489  			wantErr: `invalid module registry hostname: must contain at least one dot`,
   490  		},
   491  		"invalid target system": {
   492  			input:   `foo/var/no-no-no`,
   493  			wantErr: `invalid target system "no-no-no": must be between one and 64 ASCII letters or digits`,
   494  		},
   495  		"invalid namespace": {
   496  			input:   `boop!/var/baz`,
   497  			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`,
   498  		},
   499  		"missing part with explicit hostname": {
   500  			input:   `foo.com/var/baz`,
   501  			wantErr: `source address must have three more components after the hostname: the namespace, the name, and the target system`,
   502  		},
   503  		"errant query string": {
   504  			input:   `foo/var/baz?otherthing`,
   505  			wantErr: `module registry addresses may not include a query string portion`,
   506  		},
   507  		"github.com": {
   508  			// We don't allow using github.com like a module registry because
   509  			// that conflicts with the historically-supported shorthand for
   510  			// installing directly from GitHub-hosted git repositories.
   511  			input:   `github.com/HashiCorp/Consul/aws`,
   512  			wantErr: `can't use "github.com" as a module registry host, because it's reserved for installing directly from version control repositories`,
   513  		},
   514  		"bitbucket.org": {
   515  			// We don't allow using bitbucket.org like a module registry because
   516  			// that conflicts with the historically-supported shorthand for
   517  			// installing directly from BitBucket-hosted git repositories.
   518  			input:   `bitbucket.org/HashiCorp/Consul/aws`,
   519  			wantErr: `can't use "bitbucket.org" as a module registry host, because it's reserved for installing directly from version control repositories`,
   520  		},
   521  	}
   522  
   523  	for name, test := range tests {
   524  		t.Run(name, func(t *testing.T) {
   525  			addr, err := parseModuleSourceRegistry(test.input)
   526  
   527  			if test.wantErr != "" {
   528  				switch {
   529  				case err == nil:
   530  					t.Errorf("unexpected success\nwant error: %s", test.wantErr)
   531  				case err.Error() != test.wantErr:
   532  					t.Errorf("wrong error messages\ngot:  %s\nwant: %s", err.Error(), test.wantErr)
   533  				}
   534  				return
   535  			}
   536  
   537  			if err != nil {
   538  				t.Fatalf("unexpected error: %s", err.Error())
   539  			}
   540  
   541  			if got, want := addr.String(), test.wantString; got != want {
   542  				t.Errorf("wrong String() result\ngot:  %s\nwant: %s", got, want)
   543  			}
   544  			if got, want := addr.ForDisplay(), test.wantForDisplay; got != want {
   545  				t.Errorf("wrong ForDisplay() result\ngot:  %s\nwant: %s", got, want)
   546  			}
   547  			if got, want := addr.PackageAddr.ForRegistryProtocol(), test.wantForProtocol; got != want {
   548  				t.Errorf("wrong ForRegistryProtocol() result\ngot:  %s\nwant: %s", got, want)
   549  			}
   550  		})
   551  	}
   552  }