// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package rpcapi import ( "context" "encoding/json" "fmt" "os" "path" "strings" "testing" "github.com/apparentlymart/go-versions/versions" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-svchost/disco" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/getproviders" "github.com/hashicorp/terraform/internal/rpcapi/terraform1" "github.com/hashicorp/terraform/internal/rpcapi/terraform1/packages" ) func TestPackagesServer_ProviderPackageVersions(t *testing.T) { tcs := map[string]struct { source string expectedVersions []string expectedWarnings []string sourceFn providerSourceFn }{ "single_version": { source: "hashicorp/foo", expectedVersions: []string{"0.1.0"}, sourceFn: func(_ *disco.Disco) getproviders.Source { packages := []getproviders.PackageMeta{ { Provider: addrs.MustParseProviderSourceString("hashicorp/foo"), Version: versions.MustParseVersion("0.1.0"), }, } return getproviders.NewMockSource(packages, nil) }, }, "multiple_versions": { source: "hashicorp/foo", expectedVersions: []string{"0.1.0", "0.2.0"}, sourceFn: func(_ *disco.Disco) getproviders.Source { packages := []getproviders.PackageMeta{ { Provider: addrs.MustParseProviderSourceString("hashicorp/foo"), Version: versions.MustParseVersion("0.1.0"), }, { Provider: addrs.MustParseProviderSourceString("hashicorp/foo"), Version: versions.MustParseVersion("0.2.0"), }, } return getproviders.NewMockSource(packages, nil) }, }, "with_warnings": { source: "hashicorp/foo", expectedVersions: []string{"0.1.0"}, expectedWarnings: []string{"- warning one", "- warning two"}, sourceFn: func(_ *disco.Disco) getproviders.Source { packages := []getproviders.PackageMeta{ { Provider: addrs.MustParseProviderSourceString("hashicorp/foo"), Version: versions.MustParseVersion("0.1.0"), }, } warnings := map[addrs.Provider]getproviders.Warnings{ addrs.MustParseProviderSourceString("hashicorp/foo"): { "warning one", "warning two", }, } return getproviders.NewMockSource(packages, warnings) }, }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { service := &packagesServer{ providerSourceFn: tc.sourceFn, } response, err := service.ProviderPackageVersions(context.Background(), &packages.ProviderPackageVersions_Request{ SourceAddr: tc.source, }) if err != nil { t.Fatal(err) } if len(tc.expectedWarnings) > 0 { for _, diag := range response.Diagnostics { if diag.Severity == terraform1.Diagnostic_WARNING && diag.Summary == "Additional provider information from registry" { expected := fmt.Sprintf("The remote registry returned warnings for %s:\n%s", tc.source, strings.Join(tc.expectedWarnings, "\n")) if diff := cmp.Diff(expected, diag.Detail); len(diff) > 0 { t.Error(diff) } } } // We're expecting only one diagnostic with the warnings. if len(response.Diagnostics) > 1 { for _, diag := range response.Diagnostics { t.Errorf("unexpected diagnostics: %s", diag.Detail) } return } } else { // Otherwise we're expecting no diagnostics. if len(response.Diagnostics) > 0 { for _, diag := range response.Diagnostics { t.Errorf("unexpected diagnostics: %s", diag.Detail) } return } } if diff := cmp.Diff(tc.expectedVersions, response.Versions); len(diff) > 0 { t.Error(diff) } }) } } func TestPackagesServer_FetchProviderPackage(t *testing.T) { providerHashes := providerHashes(t) tcs := map[string]struct { // source, version, platforms, and hashes are what we're going to pass // in as the request. source string version string platforms []string hashes []string // platformLocations, and platformHashes are what we're going to use to // create our virtual provider metadata. platformLocations map[string]string platformHashes map[string][]string // diagnostics are the expected diagnostics for each platform. diagnostics map[string][]string }{ "single_version_and_platform": { source: "hashicorp/foo", version: "0.1.0", platforms: []string{"linux_amd64"}, platformLocations: map[string]string{ "linux_amd64": "terraform_provider_foo", }, }, "single_version_multiple_platforms": { source: "hashicorp/foo", version: "0.1.0", platforms: []string{"linux_amd64", "darwin_arm64"}, platformLocations: map[string]string{ "linux_amd64": "terraform_provider_foo", "darwin_arm64": "terraform_provider_bar", }, }, "single_version_and_platform_with_hashes": { source: "hashicorp/foo", version: "0.1.0", platforms: []string{"linux_amd64"}, platformLocations: map[string]string{ "linux_amd64": "terraform_provider_foo", }, platformHashes: map[string][]string{ "linux_amd64": { "h1:dJTExJ11p+lRE8FAm4HWzTw+uMEyfE6AXXxiOgl/nB0=", }, }, }, "single_version_and_platform_with_hashes_clash": { source: "hashicorp/foo", version: "0.1.0", hashes: []string{"h1:Hod4iOH+qbXMtH4orEmCem6F3T+YRPhDSNlXmOIRNuY="}, platforms: []string{"linux_amd64"}, platformLocations: map[string]string{ "linux_amd64": "terraform_provider_foo", }, platformHashes: map[string][]string{ "linux_amd64": { "h1:dJTExJ11p+lRE8FAm4HWzTw+uMEyfE6AXXxiOgl/nB0=", }, }, diagnostics: map[string][]string{ "linux_amd64": { "the local package for registry.terraform.io/hashicorp/foo 0.1.0 doesn't match any of the checksums previously recorded in the dependency lock file", }, }, }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { service := &packagesServer{ providerSourceFn: func(_ *disco.Disco) getproviders.Source { var providers []getproviders.PackageMeta for _, p := range tc.platforms { platform := parsePlatform(t, p) var authentication getproviders.PackageAuthentication if len(tc.platformHashes) > 0 { authentication = getproviders.NewPackageHashAuthentication(platform, func() []getproviders.Hash { var hashes []getproviders.Hash for _, hash := range tc.platformHashes[p] { hashes = append(hashes, getproviders.Hash(hash)) } return hashes }()) } providers = append(providers, getproviders.PackageMeta{ Provider: addrs.MustParseProviderSourceString(tc.source), Version: versions.MustParseVersion(tc.version), TargetPlatform: platform, Location: getproviders.PackageLocalDir(path.Join("testdata", "providers", tc.platformLocations[p])), Authentication: authentication, }) } return getproviders.NewMockSource(providers, nil) }, } cacheDir := t.TempDir() response, err := service.FetchProviderPackage(context.Background(), &packages.FetchProviderPackage_Request{ CacheDir: cacheDir, SourceAddr: tc.source, Version: tc.version, Platforms: tc.platforms, Hashes: tc.hashes, }) if err != nil { t.Fatal(err) } if len(response.Diagnostics) > 0 { for _, diag := range response.Diagnostics { t.Errorf("unexpected diagnostics: %s", diag.Detail) } return } if len(response.Results) != len(tc.platforms) { t.Fatalf("wrong number of results") } for ix, platform := range tc.platforms { result := response.Results[ix] if tc.diagnostics != nil && len(tc.diagnostics[platform]) > 0 { if len(result.Diagnostics) != len(tc.diagnostics[platform]) { t.Fatalf("expected %d diagnostics for %s but found %d", len(tc.diagnostics[platform]), platform, len(result.Diagnostics)) } for ix, expected := range tc.diagnostics[platform] { if !strings.Contains(result.Diagnostics[ix].Detail, expected) { t.Errorf("expected: %s\nactual: %s", expected, result.Diagnostics[ix]) } } return } else { if len(result.Diagnostics) > 0 { for _, diag := range result.Diagnostics { t.Errorf("unexpected diagnostics for %s: %s", platform, diag.Detail) } return } } if diff := cmp.Diff(providerHashes[tc.platformLocations[platform]], result.Provider.Hashes); len(diff) > 0 { t.Error(diff) } } }) } } func providerHashes(t *testing.T) map[string][]string { var hashes map[string][]string data, err := os.ReadFile("testdata/providers/hashes.json") if err != nil { t.Fatal(err) } if err := json.Unmarshal(data, &hashes); err != nil { t.Fatal(err) } return hashes } func parsePlatform(t *testing.T, raw string) getproviders.Platform { platform, err := getproviders.ParsePlatform(raw) if err != nil { t.Fatal(err) } return platform }