opentofu/internal/getproviders/registry_source_test.go
Martin Atkins ef64df950c getproviders: Prepare for having multiple valid hashes per package
As we continue iterating towards saving valid hashes for a package in a
depsfile lock file after installation and verifying them on future
installation, this prepares getproviders for the possibility of having
multiple valid hashes per package.

This will arise in future commits for two reasons:
- We will need to support both the legacy "zip hash" hashing scheme and
  the new-style content-based hashing scheme because currently the
  registry protocol is only able to produce the legacy scheme, but our
  other installation sources prefer the content-based scheme. Therefore
  packages will typically have a mixture of hashes of both types.
- Installing from an upstream registry will save the hashes for the
  packages across all supported platforms, rather than just the current
  platform, and we'll consider all of those valid for future installation
  if we see both successful matching of the current platform checksum and
  a signature verification for the checksums file as a whole.

This also includes some more preparation for the second case above in that
signatureAuthentication now supports AcceptableHashes and returns all of
the zip-based hashes it can find in the checksums file. This is a bit of
an abstraction leak because previously that authenticator considered its
"document" to just be opaque bytes, but we want to make sure that we can
only end up trusting _all_ of the hashes if we've verified that the
document is signed. Hopefully we'll make this better in a future commit
with some refactoring, but that's deferred for now in order to minimize
disruption to existing codepaths while we work towards a provider locking
MVP.
2020-09-24 14:01:54 -07:00

239 lines
7.2 KiB
Go

package getproviders
import (
"fmt"
"regexp"
"strings"
"testing"
"github.com/apparentlymart/go-versions/versions"
"github.com/google/go-cmp/cmp"
svchost "github.com/hashicorp/terraform-svchost"
"github.com/hashicorp/terraform/addrs"
)
func TestSourceAvailableVersions(t *testing.T) {
source, baseURL, close := testRegistrySource(t)
defer close()
tests := []struct {
provider string
wantVersions []string
wantErr string
}{
// These test cases are relying on behaviors of the fake provider
// registry server implemented in registry_client_test.go.
{
"example.com/awesomesauce/happycloud",
[]string{"0.1.0", "1.0.0", "1.2.0", "2.0.0"},
``,
},
{
"example.com/weaksauce/no-versions",
nil,
``, // having no versions is not an error, it's just odd
},
{
"example.com/nonexist/nonexist",
nil,
`provider registry example.com does not have a provider named example.com/nonexist/nonexist`,
},
{
"not.example.com/foo/bar",
nil,
`host not.example.com does not offer a Terraform provider registry`,
},
{
"too-new.example.com/foo/bar",
nil,
`host too-new.example.com does not support the provider registry protocol required by this Terraform version, but may be compatible with a different Terraform version`,
},
{
"fails.example.com/foo/bar",
nil,
`could not query provider registry for fails.example.com/foo/bar: the request failed after 2 attempts, please try again later: Get "` + baseURL + `/fails-immediately/foo/bar/versions": EOF`,
},
}
for _, test := range tests {
t.Run(test.provider, func(t *testing.T) {
provider := addrs.MustParseProviderSourceString(test.provider)
gotVersions, _, err := source.AvailableVersions(provider)
if err != nil {
if test.wantErr == "" {
t.Fatalf("wrong error\ngot: %s\nwant: <nil>", err.Error())
}
if got, want := err.Error(), test.wantErr; got != want {
t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want)
}
return
}
if test.wantErr != "" {
t.Fatalf("wrong error\ngot: <nil>\nwant: %s", test.wantErr)
}
var gotVersionsStr []string
if gotVersions != nil {
gotVersionsStr = make([]string, len(gotVersions))
for i, v := range gotVersions {
gotVersionsStr[i] = v.String()
}
}
if diff := cmp.Diff(test.wantVersions, gotVersionsStr); diff != "" {
t.Errorf("wrong result\n%s", diff)
}
})
}
}
func TestSourceAvailableVersions_warnings(t *testing.T) {
source, _, close := testRegistrySource(t)
defer close()
provider := addrs.MustParseProviderSourceString("example.com/weaksauce/no-versions")
_, warnings, err := source.AvailableVersions(provider)
if err != nil {
t.Fatalf("unexpected error: %s", err.Error())
}
if len(warnings) != 1 {
t.Fatalf("wrong number of warnings. Expected 1, got %d", len(warnings))
}
}
func TestSourcePackageMeta(t *testing.T) {
source, baseURL, close := testRegistrySource(t)
defer close()
tests := []struct {
provider string
version string
os, arch string
want PackageMeta
wantHashes []Hash
wantErr string
}{
// These test cases are relying on behaviors of the fake provider
// registry server implemented in registry_client_test.go.
{
"example.com/awesomesauce/happycloud",
"1.2.0",
"linux", "amd64",
PackageMeta{
Provider: addrs.NewProvider(
svchost.Hostname("example.com"), "awesomesauce", "happycloud",
),
Version: versions.MustParseVersion("1.2.0"),
ProtocolVersions: VersionList{versions.MustParseVersion("5.0.0")},
TargetPlatform: Platform{"linux", "amd64"},
Filename: "happycloud_1.2.0.zip",
Location: PackageHTTPURL(baseURL + "/pkg/awesomesauce/happycloud_1.2.0.zip"),
Authentication: PackageAuthenticationAll(
NewMatchingChecksumAuthentication(
[]byte("000000000000000000000000000000000000000000000000000000000000f00d happycloud_1.2.0.zip\n000000000000000000000000000000000000000000000000000000000000face happycloud_1.2.0_face.zip\n"),
"happycloud_1.2.0.zip",
[32]byte{30: 0xf0, 31: 0x0d},
),
NewArchiveChecksumAuthentication(Platform{"linux", "amd64"}, [32]byte{30: 0xf0, 31: 0x0d}),
NewSignatureAuthentication(
[]byte("000000000000000000000000000000000000000000000000000000000000f00d happycloud_1.2.0.zip\n000000000000000000000000000000000000000000000000000000000000face happycloud_1.2.0_face.zip\n"),
[]byte("GPG signature"),
[]SigningKey{
{ASCIIArmor: HashicorpPublicKey},
},
),
),
},
[]Hash{
"zh:000000000000000000000000000000000000000000000000000000000000f00d",
"zh:000000000000000000000000000000000000000000000000000000000000face",
},
``,
},
{
"example.com/awesomesauce/happycloud",
"1.2.0",
"nonexist", "amd64",
PackageMeta{},
nil,
`provider example.com/awesomesauce/happycloud 1.2.0 is not available for nonexist_amd64`,
},
{
"not.example.com/awesomesauce/happycloud",
"1.2.0",
"linux", "amd64",
PackageMeta{},
nil,
`host not.example.com does not offer a Terraform provider registry`,
},
{
"too-new.example.com/awesomesauce/happycloud",
"1.2.0",
"linux", "amd64",
PackageMeta{},
nil,
`host too-new.example.com does not support the provider registry protocol required by this Terraform version, but may be compatible with a different Terraform version`,
},
{
"fails.example.com/awesomesauce/happycloud",
"1.2.0",
"linux", "amd64",
PackageMeta{},
nil,
`could not query provider registry for fails.example.com/awesomesauce/happycloud: the request failed after 2 attempts, please try again later: Get "http://placeholder-origin/fails-immediately/awesomesauce/happycloud/1.2.0/download/linux/amd64": EOF`,
},
}
// Sometimes error messages contain specific HTTP endpoint URLs, but
// since our test server is on a random port we'd not be able to
// consistently match those. Instead, we'll normalize the URLs.
urlPattern := regexp.MustCompile(`http://[^/]+/`)
cmpOpts := cmp.Comparer(Version.Same)
for _, test := range tests {
t.Run(fmt.Sprintf("%s for %s_%s", test.provider, test.os, test.arch), func(t *testing.T) {
// TEMP: We don't yet have a function for parsing provider
// source addresses so we'll just fake it in here for now.
parts := strings.Split(test.provider, "/")
providerAddr := addrs.Provider{
Hostname: svchost.Hostname(parts[0]),
Namespace: parts[1],
Type: parts[2],
}
version := versions.MustParseVersion(test.version)
got, err := source.PackageMeta(providerAddr, version, Platform{test.os, test.arch})
if err != nil {
if test.wantErr == "" {
t.Fatalf("wrong error\ngot: %s\nwant: <nil>", err.Error())
}
gotErr := urlPattern.ReplaceAllLiteralString(err.Error(), "http://placeholder-origin/")
if got, want := gotErr, test.wantErr; got != want {
t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want)
}
return
}
if test.wantErr != "" {
t.Fatalf("wrong error\ngot: <nil>\nwant: %s", test.wantErr)
}
if diff := cmp.Diff(test.want, got, cmpOpts); diff != "" {
t.Errorf("wrong result\n%s", diff)
}
if diff := cmp.Diff(test.wantHashes, got.AcceptableHashes()); diff != "" {
t.Errorf("wrong AcceptableHashes result\n%s", diff)
}
})
}
}