// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package command import ( "testing" "github.com/apparentlymart/go-versions/versions" "github.com/google/go-cmp/cmp" "github.com/hashicorp/cli" tfaddr "github.com/hashicorp/terraform-registry-address" "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/getproviders/providerreqs" ) // This tests combining locks from config and state. Locks derived from state are always unconstrained, i.e. no version constraint data, // so this test func Test_mergeLockedDependencies_config_and_state(t *testing.T) { providerA := tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "my-org", "providerA") providerB := tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "my-org", "providerB") v1_0_0 := providerreqs.MustParseVersion("1.0.0") versionConstraintv1, _ := providerreqs.ParseVersionConstraints("1.0.0") hashesProviderA := []providerreqs.Hash{providerreqs.MustParseHash("providerA:this-is-providerA")} hashesProviderB := []providerreqs.Hash{providerreqs.MustParseHash("providerB:this-is-providerB")} var versionUnconstrained providerreqs.VersionConstraints = nil noVersion := versions.Version{} cases := map[string]struct { configLocks *depsfile.Locks stateLocks *depsfile.Locks expectedLocks *depsfile.Locks }{ "no locks when all inputs empty": { configLocks: depsfile.NewLocks(), stateLocks: depsfile.NewLocks(), expectedLocks: depsfile.NewLocks(), }, "when provider only described in config, output locks have matching constraints": { configLocks: func() *depsfile.Locks { configLocks := depsfile.NewLocks() configLocks.SetProvider(providerA, v1_0_0, versionConstraintv1, hashesProviderA) return configLocks }(), stateLocks: depsfile.NewLocks(), expectedLocks: func() *depsfile.Locks { combinedLocks := depsfile.NewLocks() combinedLocks.SetProvider(providerA, v1_0_0, versionConstraintv1, hashesProviderA) return combinedLocks }(), }, "when provider only described in state, output locks are unconstrained": { configLocks: depsfile.NewLocks(), stateLocks: func() *depsfile.Locks { stateLocks := depsfile.NewLocks() stateLocks.SetProvider(providerA, noVersion, versionUnconstrained, hashesProviderA) return stateLocks }(), expectedLocks: func() *depsfile.Locks { combinedLocks := depsfile.NewLocks() combinedLocks.SetProvider(providerA, noVersion, versionUnconstrained, hashesProviderA) return combinedLocks }(), }, "different providers present in state and config are combined, with version constraints kept on config providers": { configLocks: func() *depsfile.Locks { configLocks := depsfile.NewLocks() configLocks.SetProvider(providerA, v1_0_0, versionConstraintv1, hashesProviderA) return configLocks }(), stateLocks: func() *depsfile.Locks { stateLocks := depsfile.NewLocks() // Imagine that the state locks contain: // 1) provider for resources in the config stateLocks.SetProvider(providerA, noVersion, versionUnconstrained, hashesProviderA) // 2) also, a provider that's deleted from the config and only present in state stateLocks.SetProvider(providerB, noVersion, versionUnconstrained, hashesProviderB) return stateLocks }(), expectedLocks: func() *depsfile.Locks { combinedLocks := depsfile.NewLocks() combinedLocks.SetProvider(providerA, v1_0_0, versionConstraintv1, hashesProviderA) // version constraint preserved combinedLocks.SetProvider(providerB, noVersion, versionUnconstrained, hashesProviderB) // sourced from state only return combinedLocks }(), }, } for tn, tc := range cases { t.Run(tn, func(t *testing.T) { // Use tmp dir as we're creating lock files in the test td := t.TempDir() t.Chdir(td) ui := new(cli.MockUi) view, _ := testView(t) m := Meta{ Ui: ui, View: view, } // Code under test - combine deps from state with prior deps from config // mergedLocks := m.mergeLockedDependencies(tc.configLocks, tc.stateLocks) mergedLocks := m.mergeLockedDependencies(tc.configLocks, tc.stateLocks) // We cannot use (l *depsfile.Locks) Equal here as it doesn't compare version constraints // Instead, inspect entries directly if len(mergedLocks.AllProviders()) != len(tc.expectedLocks.AllProviders()) { t.Fatalf("expected merged dependencies to include %d providers, but got %d:\n %#v", len(tc.expectedLocks.AllProviders()), len(mergedLocks.AllProviders()), mergedLocks.AllProviders(), ) } for _, lock := range tc.expectedLocks.AllProviders() { match := mergedLocks.Provider(lock.Provider()) if match == nil { t.Fatalf("expected merged dependencies to include provider %s, but it's missing", lock.Provider()) } if len(match.VersionConstraints()) != len(lock.VersionConstraints()) { t.Fatalf("detected a problem with version constraints for provider %s, got: %d, want %d", lock.Provider(), len(match.VersionConstraints()), len(lock.VersionConstraints()), ) } if len(match.VersionConstraints()) > 0 && len(lock.VersionConstraints()) > 0 { gotConstraints := match.VersionConstraints()[0] wantConstraints := lock.VersionConstraints()[0] if gotConstraints.Boundary.String() != wantConstraints.Boundary.String() { t.Fatalf("expected merged dependencies to include provider %s with version constraint %v, but instead got %v", lock.Provider(), gotConstraints.Boundary.String(), wantConstraints.Boundary.String(), ) } } } if diff := cmp.Diff(tc.expectedLocks, mergedLocks); diff != "" { t.Errorf("difference in file contents detected\n%s", diff) } }) } }