diff --git a/internal/getproviders/multi_source_test.go b/internal/getproviders/multi_source_test.go new file mode 100644 index 000000000..21c69a14b --- /dev/null +++ b/internal/getproviders/multi_source_test.go @@ -0,0 +1,431 @@ +package getproviders + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/addrs" +) + +func TestMultiSourceAvailableVersions(t *testing.T) { + platform1 := Platform{OS: "amigaos", Arch: "m68k"} + platform2 := Platform{OS: "aros", Arch: "arm"} + + t.Run("unfiltered merging", func(t *testing.T) { + s1 := NewMockSource([]PackageMeta{ + FakePackageMeta( + addrs.NewDefaultProvider("foo"), + MustParseVersion("1.0.0"), + platform1, + ), + FakePackageMeta( + addrs.NewDefaultProvider("foo"), + MustParseVersion("1.0.0"), + platform2, + ), + FakePackageMeta( + addrs.NewDefaultProvider("bar"), + MustParseVersion("1.0.0"), + platform2, + ), + }) + s2 := NewMockSource([]PackageMeta{ + FakePackageMeta( + addrs.NewDefaultProvider("foo"), + MustParseVersion("1.0.0"), + platform1, + ), + FakePackageMeta( + addrs.NewDefaultProvider("foo"), + MustParseVersion("1.2.0"), + platform1, + ), + FakePackageMeta( + addrs.NewDefaultProvider("bar"), + MustParseVersion("1.0.0"), + platform1, + ), + }) + multi := MultiSource{ + {Source: s1}, + {Source: s2}, + } + + // AvailableVersions produces the union of all versions available + // across all of the sources. + got, err := multi.AvailableVersions(addrs.NewDefaultProvider("foo")) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + want := VersionList{ + MustParseVersion("1.0.0"), + MustParseVersion("1.2.0"), + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + + _, err = multi.AvailableVersions(addrs.NewDefaultProvider("baz")) + if want, ok := err.(ErrProviderNotKnown); !ok { + t.Fatalf("wrong error type:\ngot: %T\nwant: %T", err, want) + } + }) + + t.Run("merging with filters", func(t *testing.T) { + // This is just testing that filters are being honored at all, using a + // specific pair of filters. The different filter combinations + // themselves are tested in TestMultiSourceSelector. + + s1 := NewMockSource([]PackageMeta{ + FakePackageMeta( + addrs.NewDefaultProvider("foo"), + MustParseVersion("1.0.0"), + platform1, + ), + FakePackageMeta( + addrs.NewDefaultProvider("bar"), + MustParseVersion("1.0.0"), + platform1, + ), + }) + s2 := NewMockSource([]PackageMeta{ + FakePackageMeta( + addrs.NewDefaultProvider("foo"), + MustParseVersion("1.2.0"), + platform1, + ), + FakePackageMeta( + addrs.NewDefaultProvider("bar"), + MustParseVersion("1.2.0"), + platform1, + ), + }) + multi := MultiSource{ + { + Source: s1, + Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"), + }, + { + Source: s2, + Include: mustParseMultiSourceMatchingPatterns("hashicorp/bar"), + }, + } + + got, err := multi.AvailableVersions(addrs.NewDefaultProvider("foo")) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + want := VersionList{ + MustParseVersion("1.0.0"), + // 1.2.0 isn't present because s3 doesn't include "foo" + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + + got, err = multi.AvailableVersions(addrs.NewDefaultProvider("bar")) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + want = VersionList{ + MustParseVersion("1.0.0"), + MustParseVersion("1.2.0"), // included because s2 matches "bar" + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + + _, err = multi.AvailableVersions(addrs.NewDefaultProvider("baz")) + if want, ok := err.(ErrProviderNotKnown); !ok { + t.Fatalf("wrong error type:\ngot: %T\nwant: %T", err, want) + } + }) +} + +func TestMultiSourcePackageMeta(t *testing.T) { + platform1 := Platform{OS: "amigaos", Arch: "m68k"} + platform2 := Platform{OS: "aros", Arch: "arm"} + + // We'll use the Filename field of the fake PackageMetas we created above + // to create a difference between the packages in s1 and the ones in s2, + // so we can test where individual packages came from below. + fakeFilename := func(fn string, meta PackageMeta) PackageMeta { + meta.Filename = fn + return meta + } + + onlyInS1 := fakeFilename("s1", FakePackageMeta( + addrs.NewDefaultProvider("foo"), + MustParseVersion("1.0.0"), + platform2, + )) + onlyInS2 := fakeFilename("s2", FakePackageMeta( + addrs.NewDefaultProvider("foo"), + MustParseVersion("1.2.0"), + platform1, + )) + inBothS1 := fakeFilename("s1", FakePackageMeta( + addrs.NewDefaultProvider("foo"), + MustParseVersion("1.0.0"), + platform1, + )) + inBothS2 := fakeFilename("s2", inBothS1) + s1 := NewMockSource([]PackageMeta{ + inBothS1, + onlyInS1, + fakeFilename("s1", FakePackageMeta( + addrs.NewDefaultProvider("bar"), + MustParseVersion("1.0.0"), + platform2, + )), + }) + s2 := NewMockSource([]PackageMeta{ + inBothS2, + onlyInS2, + fakeFilename("s2", FakePackageMeta( + addrs.NewDefaultProvider("bar"), + MustParseVersion("1.0.0"), + platform1, + )), + }) + multi := MultiSource{ + {Source: s1}, + {Source: s2}, + } + + t.Run("only in s1", func(t *testing.T) { + got, err := multi.PackageMeta( + addrs.NewDefaultProvider("foo"), + MustParseVersion("1.0.0"), + platform2, + ) + want := onlyInS1 + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) + t.Run("only in s2", func(t *testing.T) { + got, err := multi.PackageMeta( + addrs.NewDefaultProvider("foo"), + MustParseVersion("1.2.0"), + platform1, + ) + want := onlyInS2 + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) + t.Run("in both", func(t *testing.T) { + got, err := multi.PackageMeta( + addrs.NewDefaultProvider("foo"), + MustParseVersion("1.0.0"), + platform1, + ) + want := inBothS1 // S1 "wins" because it's earlier in the MultiSource + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + + // Make sure inBothS1 and inBothS2 really are different; if not then + // that's a test bug which we'd rather catch than have this test + // accidentally passing without actually checking anything. + if diff := cmp.Diff(inBothS1, inBothS2); diff == "" { + t.Fatalf("test bug: inBothS1 and inBothS2 are indistinguishable") + } + }) + t.Run("in neither", func(t *testing.T) { + _, err := multi.PackageMeta( + addrs.NewDefaultProvider("nonexist"), + MustParseVersion("1.0.0"), + platform1, + ) + // This case reports "platform not supported" because it assumes that + // a caller would only pass to it package versions that were returned + // by a previousc all to AvailableVersions, and therefore a missing + // object ought to be valid provider/version but an unsupported + // platform. + if want, ok := err.(ErrPlatformNotSupported); !ok { + t.Fatalf("wrong error type:\ngot: %T\nwant: %T", err, want) + } + }) +} + +func TestMultiSourceSelector(t *testing.T) { + emptySource := NewMockSource(nil) + + tests := map[string]struct { + Selector MultiSourceSelector + Provider addrs.Provider + WantMatch bool + }{ + "default provider with no constraints": { + MultiSourceSelector{ + Source: emptySource, + }, + addrs.NewDefaultProvider("foo"), + true, + }, + "built-in provider with no constraints": { + MultiSourceSelector{ + Source: emptySource, + }, + addrs.NewBuiltInProvider("bar"), + true, + }, + + // Include constraints + "default provider with include constraint that matches it exactly": { + MultiSourceSelector{ + Source: emptySource, + Include: mustParseMultiSourceMatchingPatterns("hashicorp/foo"), + }, + addrs.NewDefaultProvider("foo"), + true, + }, + "default provider with include constraint that matches it via type wildcard": { + MultiSourceSelector{ + Source: emptySource, + Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"), + }, + addrs.NewDefaultProvider("foo"), + true, + }, + "default provider with include constraint that matches it via namespace wildcard": { + MultiSourceSelector{ + Source: emptySource, + Include: mustParseMultiSourceMatchingPatterns("*/*"), + }, + addrs.NewDefaultProvider("foo"), + true, + }, + "built-in provider with exact include constraint that does not match it": { + MultiSourceSelector{ + Source: emptySource, + Include: mustParseMultiSourceMatchingPatterns("hashicorp/foo"), + }, + addrs.NewBuiltInProvider("bar"), + false, + }, + "built-in provider with type-wild include constraint that does not match it": { + MultiSourceSelector{ + Source: emptySource, + Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"), + }, + addrs.NewBuiltInProvider("bar"), + false, + }, + "built-in provider with namespace-wild include constraint that does not match it": { + MultiSourceSelector{ + Source: emptySource, + Include: mustParseMultiSourceMatchingPatterns("*/*"), + }, + // Doesn't match because builtin providers are in "terraform.io", + // but a pattern with no hostname is for registry.terraform.io. + addrs.NewBuiltInProvider("bar"), + false, + }, + "built-in provider with include constraint that matches it via type wildcard": { + MultiSourceSelector{ + Source: emptySource, + Include: mustParseMultiSourceMatchingPatterns("terraform.io/builtin/*"), + }, + addrs.NewBuiltInProvider("bar"), + true, + }, + + // Exclude constraints + "default provider with exclude constraint that matches it exactly": { + MultiSourceSelector{ + Source: emptySource, + Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/foo"), + }, + addrs.NewDefaultProvider("foo"), + false, + }, + "default provider with exclude constraint that matches it via type wildcard": { + MultiSourceSelector{ + Source: emptySource, + Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/*"), + }, + addrs.NewDefaultProvider("foo"), + false, + }, + "default provider with exact exclude constraint that doesn't match it": { + MultiSourceSelector{ + Source: emptySource, + Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/bar"), + }, + addrs.NewDefaultProvider("foo"), + true, + }, + + // Both include and exclude in a single selector + "default provider with exclude wildcard overriding include exact": { + MultiSourceSelector{ + Source: emptySource, + Include: mustParseMultiSourceMatchingPatterns("hashicorp/foo"), + Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/*"), + }, + addrs.NewDefaultProvider("foo"), + false, + }, + "default provider with exclude wildcard overriding irrelevant include exact": { + MultiSourceSelector{ + Source: emptySource, + Include: mustParseMultiSourceMatchingPatterns("hashicorp/bar"), + Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/*"), + }, + addrs.NewDefaultProvider("foo"), + false, + }, + "default provider with exclude exact overriding include wildcard": { + MultiSourceSelector{ + Source: emptySource, + Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"), + Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/foo"), + }, + addrs.NewDefaultProvider("foo"), + false, + }, + "default provider with irrelevant exclude exact overriding include wildcard": { + MultiSourceSelector{ + Source: emptySource, + Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"), + Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/bar"), + }, + addrs.NewDefaultProvider("foo"), + true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Logf("include: %s", test.Selector.Include) + t.Logf("exclude: %s", test.Selector.Exclude) + t.Logf("provider: %s", test.Provider) + got := test.Selector.CanHandleProvider(test.Provider) + want := test.WantMatch + if got != want { + t.Errorf("wrong result %t; want %t", got, want) + } + }) + } +} + +func mustParseMultiSourceMatchingPatterns(strs ...string) MultiSourceMatchingPatterns { + ret, err := ParseMultiSourceMatchingPatterns(strs) + if err != nil { + panic(err) + } + return ret +}