// Copyright 2016-2024, Pulumi Corporation. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package deploy import ( "runtime" "testing" "github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest" "github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" "github.com/stretchr/testify/assert" ) func TestIgnoreChanges(t *testing.T) { t.Parallel() cases := []struct { name string oldInputs map[string]interface{} newInputs map[string]interface{} expected map[string]interface{} ignoreChanges []string expectFailure bool }{ { name: "Present in old and new sets", oldInputs: map[string]interface{}{ "a": map[string]interface{}{ "b": "foo", }, }, newInputs: map[string]interface{}{ "a": map[string]interface{}{ "b": "bar", }, "c": 42, }, expected: map[string]interface{}{ "a": map[string]interface{}{ "b": "foo", }, "c": 42, }, ignoreChanges: []string{"a.b"}, }, { name: "Missing in new sets", oldInputs: map[string]interface{}{ "a": map[string]interface{}{ "b": "foo", }, }, newInputs: map[string]interface{}{ "a": map[string]interface{}{}, "c": 42, }, expected: map[string]interface{}{ "a": map[string]interface{}{ "b": "foo", }, "c": 42, }, ignoreChanges: []string{"a.b"}, }, { name: "Present in old and new sets, using [\"\"]", oldInputs: map[string]interface{}{ "a": map[string]interface{}{ "b": map[string]interface{}{ "c": "foo", }, }, }, newInputs: map[string]interface{}{ "a": map[string]interface{}{ "b": map[string]interface{}{ "c": "bar", }, }, "c": 42, }, expected: map[string]interface{}{ "a": map[string]interface{}{ "b": map[string]interface{}{ "c": "foo", }, }, "c": 42, }, ignoreChanges: []string{"a.b[\"c\"]"}, }, { name: "Missing in new sets, using [\"\"]", oldInputs: map[string]interface{}{ "a": map[string]interface{}{ "b": map[string]interface{}{ "c": "foo", }, }, }, newInputs: map[string]interface{}{ "a": map[string]interface{}{ "b": map[string]interface{}{}, }, "c": 42, }, expected: map[string]interface{}{ "a": map[string]interface{}{ "b": map[string]interface{}{ "c": "foo", }, }, "c": 42, }, ignoreChanges: []string{"a.b[\"c\"]"}, }, { name: "Missing in old deletes", oldInputs: map[string]interface{}{}, newInputs: map[string]interface{}{ "a": map[string]interface{}{ "b": "foo", }, "c": 42, }, expected: map[string]interface{}{ "a": map[string]interface{}{}, "c": 42, }, ignoreChanges: []string{"a.b"}, }, { name: "Missing keys in old and new are OK", oldInputs: map[string]interface{}{}, newInputs: map[string]interface{}{}, ignoreChanges: []string{ "a", "a.b", "a.c[0]", }, }, { name: "Missing parent keys in only new fail", oldInputs: map[string]interface{}{ "a": map[string]interface{}{ "b": "foo", }, }, newInputs: map[string]interface{}{}, ignoreChanges: []string{"a.b"}, expectFailure: true, }, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { t.Parallel() olds, news := resource.NewPropertyMapFromMap(c.oldInputs), resource.NewPropertyMapFromMap(c.newInputs) expected := olds if c.expected != nil { expected = resource.NewPropertyMapFromMap(c.expected) } processed, err := processIgnoreChanges(news, olds, c.ignoreChanges) if c.expectFailure { assert.Error(t, err) } else { assert.NoError(t, err) assert.Equal(t, expected, processed) } }) } } func TestApplyReplaceOnChangesEmptyDetailedDiff(t *testing.T) { t.Parallel() cases := []struct { name string diff plugin.DiffResult replaceOnChanges []string hasInitErrors bool expected plugin.DiffResult }{ { name: "Empty diff and replaceOnChanges", diff: plugin.DiffResult{}, replaceOnChanges: []string{}, hasInitErrors: false, expected: plugin.DiffResult{}, }, { name: "DiffSome and empty replaceOnChanges", diff: plugin.DiffResult{ Changes: plugin.DiffSome, ChangedKeys: []resource.PropertyKey{"a"}, }, replaceOnChanges: []string{}, hasInitErrors: false, expected: plugin.DiffResult{ Changes: plugin.DiffSome, ChangedKeys: []resource.PropertyKey{"a"}, }, }, { name: "DiffSome and non-empty replaceOnChanges", diff: plugin.DiffResult{ Changes: plugin.DiffSome, ChangedKeys: []resource.PropertyKey{"a"}, }, replaceOnChanges: []string{"a"}, hasInitErrors: false, expected: plugin.DiffResult{ Changes: plugin.DiffSome, ChangedKeys: []resource.PropertyKey{"a"}, ReplaceKeys: []resource.PropertyKey{"a"}, }, }, { name: "Empty diff and replaceOnChanges w/ init errors", diff: plugin.DiffResult{}, replaceOnChanges: []string{}, hasInitErrors: true, expected: plugin.DiffResult{}, }, { name: "DiffSome and empty replaceOnChanges w/ init errors", diff: plugin.DiffResult{ Changes: plugin.DiffSome, ChangedKeys: []resource.PropertyKey{"a"}, }, replaceOnChanges: []string{}, hasInitErrors: true, expected: plugin.DiffResult{ Changes: plugin.DiffSome, ChangedKeys: []resource.PropertyKey{"a"}, }, }, { name: "DiffSome and non-empty replaceOnChanges w/ init errors", diff: plugin.DiffResult{ Changes: plugin.DiffSome, ChangedKeys: []resource.PropertyKey{"a"}, }, replaceOnChanges: []string{"a"}, hasInitErrors: true, expected: plugin.DiffResult{ Changes: plugin.DiffSome, ChangedKeys: []resource.PropertyKey{"a"}, ReplaceKeys: []resource.PropertyKey{"a"}, }, }, { name: "Empty diff and non-empty replaceOnChanges w/ init errors", diff: plugin.DiffResult{}, replaceOnChanges: []string{"*"}, hasInitErrors: true, expected: plugin.DiffResult{ Changes: plugin.DiffSome, ReplaceKeys: []resource.PropertyKey{"#initerror"}, }, }, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { t.Parallel() newdiff, err := applyReplaceOnChanges(c.diff, c.replaceOnChanges, c.hasInitErrors) assert.NoError(t, err) assert.Equal(t, c.expected, newdiff) }) } } func TestEngineDiff(t *testing.T) { t.Parallel() cases := []struct { name string oldInputs, newInputs resource.PropertyMap ignoreChanges []string expected []resource.PropertyKey expectedChanges plugin.DiffChanges }{ { name: "Empty diff", oldInputs: resource.NewPropertyMapFromMap(map[string]interface{}{ "val1": resource.NewPropertyValue(8), "val2": resource.NewPropertyValue("hello"), }), newInputs: resource.NewPropertyMapFromMap(map[string]interface{}{ "val1": resource.NewPropertyValue(8), "val2": resource.NewPropertyValue("hello"), }), expected: nil, expectedChanges: plugin.DiffNone, }, { name: "All changes", oldInputs: resource.NewPropertyMapFromMap(map[string]interface{}{ "val0": resource.NewPropertyValue(3.14), }), newInputs: resource.NewPropertyMapFromMap(map[string]interface{}{ "val1": resource.NewNumberProperty(42), "val2": resource.NewPropertyValue("world"), }), expected: []resource.PropertyKey{"val0", "val1", "val2"}, expectedChanges: plugin.DiffSome, }, { name: "Some changes", oldInputs: resource.NewPropertyMapFromMap(map[string]interface{}{ "val1": resource.NewPropertyValue(42), }), newInputs: resource.NewPropertyMapFromMap(map[string]interface{}{ "val1": resource.NewNumberProperty(42), "val2": resource.NewPropertyValue("world"), }), expected: []resource.PropertyKey{"val2"}, expectedChanges: plugin.DiffSome, }, { name: "Ignore some changes", oldInputs: resource.NewPropertyMapFromMap(map[string]interface{}{ "val1": resource.NewPropertyValue("hello"), }), newInputs: resource.NewPropertyMapFromMap(map[string]interface{}{ "val2": resource.NewPropertyValue(8), }), ignoreChanges: []string{"val1"}, expected: []resource.PropertyKey{"val2"}, expectedChanges: plugin.DiffSome, }, { name: "Ignore all changes", oldInputs: resource.NewPropertyMapFromMap(map[string]interface{}{ "val1": resource.NewPropertyValue("hello"), }), newInputs: resource.NewPropertyMapFromMap(map[string]interface{}{ "val2": resource.NewPropertyValue(8), }), ignoreChanges: []string{"val1", "val2"}, expected: nil, expectedChanges: plugin.DiffNone, }, } urn := resource.URN("urn:pulumi:dev::website-and-lambda::aws:s3/bucket:Bucket::my-bucket") id := resource.ID("someid") var oldOutputs resource.PropertyMap allowUnknowns := false provider := deploytest.Provider{} for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { t.Parallel() diff, err := diffResource(urn, id, c.oldInputs, oldOutputs, c.newInputs, &provider, allowUnknowns, c.ignoreChanges) t.Logf("diff.ChangedKeys = %v", diff.ChangedKeys) t.Logf("diff.StableKeys = %v", diff.StableKeys) t.Logf("diff.ReplaceKeys = %v", diff.ReplaceKeys) assert.NoError(t, err) assert.Equal(t, c.expectedChanges, diff.Changes) assert.EqualValues(t, c.expected, diff.ChangedKeys) }) } } func TestGenerateAliases(t *testing.T) { t.Parallel() const ( project = "project" ) stack := tokens.MustParseStackName("stack") parentTypeAlias := resource.CreateURN("myres", "test:resource:type2", "", project, stack.String()) parentNameAlias := resource.CreateURN("myres2", "test:resource:type", "", project, stack.String()) cases := []struct { name string parentAlias *resource.URN childAliases []resource.Alias expected []resource.URN }{ { name: "no aliases", expected: nil, }, { name: "child alias (type), no parent aliases", childAliases: []resource.Alias{ {Type: "test:resource:child2"}, }, expected: []resource.URN{ "urn:pulumi:stack::project::test:resource:type$test:resource:child2::myres-child", }, }, { name: "child alias (name), no parent aliases", childAliases: []resource.Alias{ {Name: "child2"}, }, expected: []resource.URN{ "urn:pulumi:stack::project::test:resource:type$test:resource:child::child2", }, }, { name: "child alias (type, noParent), no parent aliases", childAliases: []resource.Alias{ { Type: "test:resource:child2", NoParent: true, }, }, expected: []resource.URN{ "urn:pulumi:stack::project::test:resource:child2::myres-child", }, }, { name: "child alias (type, parent), no parent aliases", childAliases: []resource.Alias{ { Type: "test:resource:child2", Parent: resource.CreateURN("originalparent", "test:resource:original", "", project, stack.String()), }, }, expected: []resource.URN{ "urn:pulumi:stack::project::test:resource:original$test:resource:child2::myres-child", }, }, { name: "child alias (name), parent alias (type)", parentAlias: &parentTypeAlias, childAliases: []resource.Alias{ {Name: "myres-child2"}, }, expected: []resource.URN{ "urn:pulumi:stack::project::test:resource:type$test:resource:child::myres-child2", "urn:pulumi:stack::project::test:resource:type2$test:resource:child::myres-child", "urn:pulumi:stack::project::test:resource:type2$test:resource:child::myres-child2", }, }, { name: "child alias (name), parent alias (name)", parentAlias: &parentNameAlias, childAliases: []resource.Alias{ {Name: "myres-child2"}, }, expected: []resource.URN{ "urn:pulumi:stack::project::test:resource:type$test:resource:child::myres-child2", "urn:pulumi:stack::project::test:resource:type$test:resource:child::myres2-child", "urn:pulumi:stack::project::test:resource:type$test:resource:child::myres2-child2", }, }, } for _, tt := range cases { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() parentURN := resource.CreateURN("myres", "test:resource:type", "", project, stack.String()) goal := &resource.Goal{ Parent: parentURN, Name: "myres-child", Type: "test:resource:child", Aliases: tt.childAliases, } sg := newStepGenerator(&Deployment{ opts: &Options{}, target: &Target{ Name: stack, }, source: NewNullSource(project), }) if tt.parentAlias != nil { sg.aliases = map[resource.URN]resource.URN{ parentURN: *tt.parentAlias, } } actual := sg.generateAliases(goal) assert.Equal(t, tt.expected, actual) }) } } func TestDeleteProtectedErrorUsesCorrectQuotesOnOS(t *testing.T) { t.Parallel() err := deleteProtectedError{urn: "resource:urn"} expectations := map[string]string{ `windows`: `"`, `linux`: `'`, `darwin`: `'`, } t.Run(runtime.GOOS, func(t *testing.T) { t.Parallel() gotErrMsg := err.Error() contains, ok := expectations[runtime.GOOS] if !ok { t.Skipf("no quoting expectation for %s", runtime.GOOS) return } assert.Contains(t, gotErrMsg, contains) }) } func TestStepGenerator(t *testing.T) { t.Parallel() t.Run("isTargetedForUpdate (no target dependents)", func(t *testing.T) { t.Parallel() apUrn := resource.NewURN("test", "test", "", providers.MakeProviderType("pkgA"), "a") apRef, err := providers.NewReference(apUrn, "0") assert.NoError(t, err) bpUrn := resource.NewURN("test", "test", "", providers.MakeProviderType("pkgB"), "b") bpRef, err := providers.NewReference(bpUrn, "1") assert.NoError(t, err) // Arrange. sg := &stepGenerator{ deployment: &Deployment{ opts: &Options{ TargetDependents: false, Targets: UrnTargets{ literals: []resource.URN{"b"}, }, }, }, targetsActual: UrnTargets{ literals: []resource.URN{bpRef.URN()}, }, } t.Run("is targeted directly", func(t *testing.T) { t.Parallel() // Arrange. a := &resource.State{URN: "a"} b := &resource.State{URN: "b"} // Act. aIsTargeted := sg.isTargetedForUpdate(a) bIsTargeted := sg.isTargetedForUpdate(b) // Assert. assert.False(t, aIsTargeted) assert.True(t, bIsTargeted) }) t.Run("has a targeted provider", func(t *testing.T) { t.Parallel() // Arrange. hasAAsProvider := &resource.State{Provider: apRef.String()} hasBAsProvider := &resource.State{Provider: bpRef.String()} // Act. hasAAsProviderIsTargeted := sg.isTargetedForUpdate(hasAAsProvider) hasBAsProviderIsTargeted := sg.isTargetedForUpdate(hasBAsProvider) // Assert. assert.False(t, hasAAsProviderIsTargeted) assert.False(t, hasBAsProviderIsTargeted) }) t.Run("has a targeted parent", func(t *testing.T) { t.Parallel() // Arrange. hasAAsParent := &resource.State{Parent: "a"} hasBAsParent := &resource.State{Parent: "b"} // Act. hasAAsParentIsTargeted := sg.isTargetedForUpdate(hasAAsParent) hasBAsParentIsTargeted := sg.isTargetedForUpdate(hasBAsParent) // Assert. assert.False(t, hasAAsParentIsTargeted) assert.False(t, hasBAsParentIsTargeted) }) t.Run("has a targeted dependency", func(t *testing.T) { t.Parallel() // Arrange. dependsOnA := &resource.State{Dependencies: []resource.URN{"a"}} dependsOnB := &resource.State{Dependencies: []resource.URN{"a", "b"}} // Act. dependsOnAIsTargeted := sg.isTargetedForUpdate(dependsOnA) dependsOnBIsTargeted := sg.isTargetedForUpdate(dependsOnB) // Assert. assert.False(t, dependsOnAIsTargeted) assert.False(t, dependsOnBIsTargeted) }) t.Run("has a targeted property dependency", func(t *testing.T) { t.Parallel() // Arrange. dependsOnA := &resource.State{PropertyDependencies: map[resource.PropertyKey][]resource.URN{"p": {"a"}}} dependsOnB := &resource.State{PropertyDependencies: map[resource.PropertyKey][]resource.URN{"p": {"a", "b"}}} // Act. dependsOnAIsTargeted := sg.isTargetedForUpdate(dependsOnA) dependsOnBIsTargeted := sg.isTargetedForUpdate(dependsOnB) // Assert. assert.False(t, dependsOnAIsTargeted) assert.False(t, dependsOnBIsTargeted) }) t.Run("is deleted with a target", func(t *testing.T) { t.Parallel() // Arrange. isDeletedWithA := &resource.State{DeletedWith: "a"} isDeletedWithB := &resource.State{DeletedWith: "b"} // Act. isDeletedWithAIsTargeted := sg.isTargetedForUpdate(isDeletedWithA) isDeletedWithBIsTargeted := sg.isTargetedForUpdate(isDeletedWithB) // Assert. assert.False(t, isDeletedWithAIsTargeted) assert.False(t, isDeletedWithBIsTargeted) }) }) t.Run("isTargetedForUpdate (target dependents)", func(t *testing.T) { t.Parallel() // Arrange. apUrn := resource.NewURN("test", "test", "", providers.MakeProviderType("pkgA"), "a") apRef, err := providers.NewReference(apUrn, "0") assert.NoError(t, err) bpUrn := resource.NewURN("test", "test", "", providers.MakeProviderType("pkgB"), "b") bpRef, err := providers.NewReference(bpUrn, "1") assert.NoError(t, err) sg := &stepGenerator{ deployment: &Deployment{ opts: &Options{ TargetDependents: true, Targets: UrnTargets{ literals: []resource.URN{"c"}, }, }, }, targetsActual: UrnTargets{ literals: []resource.URN{"b", bpRef.URN()}, }, } t.Run("is targeted directly", func(t *testing.T) { t.Parallel() // Arrange. a := &resource.State{URN: "a"} c := &resource.State{URN: "c"} // Act. aIsTargeted := sg.isTargetedForUpdate(a) cIsTargeted := sg.isTargetedForUpdate(c) // Assert. assert.False(t, aIsTargeted) assert.True(t, cIsTargeted) }) t.Run("has a targeted provider", func(t *testing.T) { t.Parallel() // Arrange. hasAAsProvider := &resource.State{Provider: apRef.String()} hasBAsProvider := &resource.State{Provider: bpRef.String()} // Act. hasAAsProviderIsTargeted := sg.isTargetedForUpdate(hasAAsProvider) hasBAsProviderIsTargeted := sg.isTargetedForUpdate(hasBAsProvider) // Assert. assert.False(t, hasAAsProviderIsTargeted) assert.True(t, hasBAsProviderIsTargeted) }) t.Run("has a targeted parent", func(t *testing.T) { t.Parallel() // Arrange. hasAAsParent := &resource.State{Parent: "a"} hasBAsParent := &resource.State{Parent: "b"} // Act. hasAAsParentIsTargeted := sg.isTargetedForUpdate(hasAAsParent) hasBAsParentIsTargeted := sg.isTargetedForUpdate(hasBAsParent) // Assert. assert.False(t, hasAAsParentIsTargeted) assert.True(t, hasBAsParentIsTargeted) }) t.Run("has a targeted dependency", func(t *testing.T) { t.Parallel() // Arrange. dependsOnA := &resource.State{Dependencies: []resource.URN{"a"}} dependsOnB := &resource.State{Dependencies: []resource.URN{"a", "b"}} // Act. dependsOnAIsTargeted := sg.isTargetedForUpdate(dependsOnA) dependsOnBIsTargeted := sg.isTargetedForUpdate(dependsOnB) // Assert. assert.False(t, dependsOnAIsTargeted) assert.True(t, dependsOnBIsTargeted) }) t.Run("has a targeted property dependency", func(t *testing.T) { t.Parallel() // Arrange. dependsOnA := &resource.State{PropertyDependencies: map[resource.PropertyKey][]resource.URN{"p": {"a"}}} dependsOnB := &resource.State{PropertyDependencies: map[resource.PropertyKey][]resource.URN{"p": {"a", "b"}}} // Act. dependsOnAIsTargeted := sg.isTargetedForUpdate(dependsOnA) dependsOnBIsTargeted := sg.isTargetedForUpdate(dependsOnB) // Assert. assert.False(t, dependsOnAIsTargeted) assert.True(t, dependsOnBIsTargeted) }) t.Run("is deleted with a target", func(t *testing.T) { t.Parallel() // Arrange. isDeletedWithA := &resource.State{DeletedWith: "a"} isDeletedWithB := &resource.State{DeletedWith: "b"} // Act. isDeletedWithAIsTargeted := sg.isTargetedForUpdate(isDeletedWithA) isDeletedWithBIsTargeted := sg.isTargetedForUpdate(isDeletedWithB) // Assert. assert.False(t, isDeletedWithAIsTargeted) assert.True(t, isDeletedWithBIsTargeted) }) }) t.Run("checkParent", func(t *testing.T) { t.Parallel() t.Run("could not find parent resource", func(t *testing.T) { t.Parallel() sg := &stepGenerator{ urns: map[resource.URN]bool{}, } _, err := sg.checkParent("does-not-exist", "") assert.ErrorContains(t, err, "could not find parent resource") }) }) t.Run("GenerateReadSteps", func(t *testing.T) { t.Parallel() t.Run("could not find parent resource", func(t *testing.T) { t.Parallel() sg := &stepGenerator{ urns: map[resource.URN]bool{}, } _, err := sg.GenerateReadSteps(&readResourceEvent{ parent: "does-not-exist", }) assert.ErrorContains(t, err, "could not find parent resource") }) t.Run("fail generateURN", func(t *testing.T) { t.Parallel() sg := &stepGenerator{ urns: map[resource.URN]bool{ "urn:pulumi:stack::::::": true, }, deployment: &Deployment{ ctx: &plugin.Context{ Diag: &deploytest.NoopSink{}, }, opts: &Options{}, target: &Target{ Name: tokens.MustParseStackName("stack"), }, source: &nullSource{}, }, } _, err := sg.GenerateReadSteps(&readResourceEvent{}) assert.ErrorContains(t, err, "Duplicate resource URN") }) }) t.Run("generateSteps", func(t *testing.T) { t.Parallel() t.Run("could not find parent resource", func(t *testing.T) { t.Parallel() sg := &stepGenerator{ urns: map[resource.URN]bool{}, deployment: &Deployment{ target: &Target{ Name: tokens.MustParseStackName("stack"), }, source: &nullSource{}, }, } _, err := sg.generateSteps(®isterResourceEvent{ goal: &resource.Goal{ Parent: "does-not-exist", }, }) assert.ErrorContains(t, err, "could not find parent resource") }) }) t.Run("determineAllowedResourcesToDeleteFromTargets", func(t *testing.T) { t.Parallel() t.Run("handle non-existent target", func(t *testing.T) { t.Parallel() sg := &stepGenerator{ urns: map[resource.URN]bool{}, deployment: &Deployment{ prev: &Snapshot{ Resources: []*resource.State{ { URN: "a", }, }, }, olds: map[resource.URN]*resource.State{}, }, } targets, err := sg.determineAllowedResourcesToDeleteFromTargets(UrnTargets{ literals: []resource.URN{"a"}, }) assert.NoError(t, err) assert.Empty(t, targets) }) }) t.Run("providerChanged", func(t *testing.T) { t.Parallel() t.Run("invalid old ProviderReference", func(t *testing.T) { t.Parallel() sg := &stepGenerator{ urns: map[resource.URN]bool{}, deployment: &Deployment{ prev: &Snapshot{}, olds: map[resource.URN]*resource.State{}, }, } _, err := sg.providerChanged("", &resource.State{ Provider: "invalid-old-provider", }, &resource.State{ Provider: "urn:pulumi:stack::project::pulumi:providers:provider::name::uuid", }, ) assert.ErrorContains(t, err, "expected '::' in provider reference 'invalid-old-provider'") }) t.Run("invalid new ProviderReference", func(t *testing.T) { t.Parallel() sg := &stepGenerator{ urns: map[resource.URN]bool{}, deployment: &Deployment{ prev: &Snapshot{}, olds: map[resource.URN]*resource.State{}, }, } _, err := sg.providerChanged("", &resource.State{ Provider: "urn:pulumi:stack::project::pulumi:providers:provider::name::uuid", }, &resource.State{ Provider: "invalid-new-provider", }, ) assert.ErrorContains(t, err, "expected '::' in provider reference 'invalid-new-provider'") }) t.Run("error getting new default provider", func(t *testing.T) { t.Parallel() sg := &stepGenerator{ urns: map[resource.URN]bool{}, deployment: &Deployment{ prev: &Snapshot{}, olds: map[resource.URN]*resource.State{}, providers: &providers.Registry{}, }, } _, err := sg.providerChanged("", &resource.State{ Provider: "urn:pulumi:stack::project::pulumi:providers:provider::default_name::uuid", }, &resource.State{ Provider: "urn:pulumi:stack::project::pulumi:providers:provider::default_new::uuid", }, ) assert.ErrorContains(t, err, "failed to resolve provider reference") }) }) }