diff --git a/.changes/unreleased/FEATURES-20240717-155731.yaml b/.changes/unreleased/FEATURES-20240717-155731.yaml new file mode 100644 index 000000000..6f06b4eb9 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240717-155731.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'compare: Introduced new `compare` package, which contains interfaces and implementations + for value comparisons in state checks.' +time: 2024-07-17T15:57:31.637692-04:00 +custom: + Issue: "330" diff --git a/.changes/unreleased/FEATURES-20240717-160116.yaml b/.changes/unreleased/FEATURES-20240717-160116.yaml new file mode 100644 index 000000000..6783377d1 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240717-160116.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'statecheck: Added `CompareValue` state check, which compares sequential values of the + specified attribute at the given managed resource, or data source, using the supplied value comparer.' +time: 2024-07-17T16:01:16.194665-04:00 +custom: + Issue: "330" diff --git a/.changes/unreleased/FEATURES-20240717-160331.yaml b/.changes/unreleased/FEATURES-20240717-160331.yaml new file mode 100644 index 000000000..1187e32a3 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240717-160331.yaml @@ -0,0 +1,7 @@ +kind: FEATURES +body: 'statecheck: Added `CompareValueCollection` state check, which compares each item in + the specified collection (e.g., list, set) attribute, with the second specified + attribute at the given managed resources, or data sources, using the supplied value comparer.' +time: 2024-07-17T16:03:31.77827-04:00 +custom: + Issue: "330" diff --git a/.changes/unreleased/FEATURES-20240717-164418.yaml b/.changes/unreleased/FEATURES-20240717-164418.yaml new file mode 100644 index 000000000..312311b1f --- /dev/null +++ b/.changes/unreleased/FEATURES-20240717-164418.yaml @@ -0,0 +1,7 @@ +kind: FEATURES +body: 'statecheck: Added `CompareValuePairs` state check, which compares the + specified attributes at the given managed resources, or data sources, using + the supplied value comparer.' +time: 2024-07-17T16:44:18.612874-04:00 +custom: + Issue: "330" diff --git a/.changes/unreleased/NOTES-20240717-155810.yaml b/.changes/unreleased/NOTES-20240717-155810.yaml new file mode 100644 index 000000000..a8edd41c4 --- /dev/null +++ b/.changes/unreleased/NOTES-20240717-155810.yaml @@ -0,0 +1,6 @@ +kind: NOTES +body: 'compare: The `compare` package is considered experimental and may be altered + or removed in a subsequent release' +time: 2024-07-17T15:58:10.435384-04:00 +custom: + Issue: "330" diff --git a/.changes/unreleased/NOTES-20240717-164911.yaml b/.changes/unreleased/NOTES-20240717-164911.yaml new file mode 100644 index 000000000..bc3af510b --- /dev/null +++ b/.changes/unreleased/NOTES-20240717-164911.yaml @@ -0,0 +1,7 @@ +kind: NOTES +body: 'statecheck: `CompareValue`, `CompareValueCollection`, and `CompareValuePairs` + state checks are considered experimental and may be altered or removed in a subsequent + release.' +time: 2024-07-17T16:49:11.296585-04:00 +custom: + Issue: "330" diff --git a/compare/doc.go b/compare/doc.go new file mode 100644 index 000000000..feb4a4c00 --- /dev/null +++ b/compare/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package compare contains the value comparer interface, and types implementing the value comparer interface. +package compare diff --git a/compare/value_comparer.go b/compare/value_comparer.go new file mode 100644 index 000000000..af635898b --- /dev/null +++ b/compare/value_comparer.go @@ -0,0 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package compare + +// ValueComparer defines an interface that is implemented to run comparison logic on multiple values. Individual +// implementations determine how the comparison is performed (e.g., values differ, values equal). +type ValueComparer interface { + // CompareValues should assert the given known values against any expectations. + // Values are always ordered in the order they were added. Use the error + // return to signal unexpected values or implementation errors. + CompareValues(values ...any) error +} diff --git a/compare/values_differ.go b/compare/values_differ.go new file mode 100644 index 000000000..24bd2ae22 --- /dev/null +++ b/compare/values_differ.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package compare + +import ( + "fmt" + "reflect" +) + +var _ ValueComparer = valuesDiffer{} + +type valuesDiffer struct{} + +// CompareValues determines whether each value in the sequence of the supplied values +// differs from the preceding value. +func (v valuesDiffer) CompareValues(values ...any) error { + for i := 1; i < len(values); i++ { + if reflect.DeepEqual(values[i-1], values[i]) { + return fmt.Errorf("expected values to differ, but they are the same: %v == %v", values[i-1], values[i]) + } + } + + return nil +} + +// ValuesDiffer returns a ValueComparer for asserting that each value in the sequence of +// the values supplied to the CompareValues method differs from the preceding value. +func ValuesDiffer() valuesDiffer { + return valuesDiffer{} +} diff --git a/compare/values_differ_test.go b/compare/values_differ_test.go new file mode 100644 index 000000000..35653f339 --- /dev/null +++ b/compare/values_differ_test.go @@ -0,0 +1,62 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package compare_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/compare" +) + +func TestValuesDiffer_CompareValues(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in []any + expectedError error + }{ + "nil": {}, + "single-value": { + in: []any{"str"}, + }, + "non-matching-sequential-values": { + in: []any{"str", "other_str", "str"}, + }, + "matching-values-string": { + in: []any{"str", "other_str", "other_str"}, + expectedError: fmt.Errorf("expected values to differ, but they are the same: other_str == other_str"), + }, + "matching-values-slice": { + in: []any{ + []any{"other_str"}, + []any{"other_str"}, + }, + expectedError: fmt.Errorf("expected values to differ, but they are the same: [other_str] == [other_str]"), + }, + "matching-values-map": { + in: []any{ + map[string]any{"a": "other_str"}, + map[string]any{"a": "other_str"}, + }, + expectedError: fmt.Errorf("expected values to differ, but they are the same: map[a:other_str] == map[a:other_str]"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + err := compare.ValuesDiffer().CompareValues(testCase.in...) + + if diff := cmp.Diff(err, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/compare/values_same.go b/compare/values_same.go new file mode 100644 index 000000000..46ee13f31 --- /dev/null +++ b/compare/values_same.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package compare + +import ( + "fmt" + "reflect" +) + +var _ ValueComparer = valuesSame{} + +type valuesSame struct{} + +// CompareValues determines whether each value in the sequence of the supplied values +// is the same as the preceding value. +func (v valuesSame) CompareValues(values ...any) error { + for i := 1; i < len(values); i++ { + if !reflect.DeepEqual(values[i-1], values[i]) { + return fmt.Errorf("expected values to be the same, but they differ: %v != %v", values[i-1], values[i]) + } + } + + return nil +} + +// ValuesSame returns a ValueComparer for asserting that each value in the sequence of +// the values supplied to the CompareValues method is the same as the preceding value. +func ValuesSame() valuesSame { + return valuesSame{} +} diff --git a/compare/values_same_test.go b/compare/values_same_test.go new file mode 100644 index 000000000..dde2ee6ea --- /dev/null +++ b/compare/values_same_test.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package compare_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/compare" +) + +func TestValuesSame_CompareValues(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in []any + expectedError error + }{ + "nil": {}, + "single-value": { + in: []any{"str"}, + }, + "matching-values": { + in: []any{"str", "str", "str"}, + }, + "non-matching-values-string": { + in: []any{"str", "str", "other_str"}, + expectedError: fmt.Errorf("expected values to be the same, but they differ: str != other_str"), + }, + "non-matching-values-slice": { + in: []any{ + []any{"str"}, + []any{"str"}, + []any{"other_str"}, + }, + expectedError: fmt.Errorf("expected values to be the same, but they differ: [str] != [other_str]"), + }, + "non-matching-values-map": { + in: []any{ + map[string]any{"a": "str"}, + map[string]any{"a": "str"}, + map[string]any{"a": "other_str"}, + }, + expectedError: fmt.Errorf("expected values to be the same, but they differ: map[a:str] != map[a:other_str]"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + err := compare.ValuesSame().CompareValues(testCase.in...) + + if diff := cmp.Diff(err, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +// equateErrorMessage reports errors to be equal if both are nil +// or both have the same message. +var equateErrorMessage = cmp.Comparer(func(x, y error) bool { + if x == nil || y == nil { + return x == nil && y == nil + } + return x.Error() == y.Error() +}) diff --git a/statecheck/compare_value.go b/statecheck/compare_value.go new file mode 100644 index 000000000..68a6ef9d5 --- /dev/null +++ b/statecheck/compare_value.go @@ -0,0 +1,114 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck + +import ( + "context" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource State Check +var _ StateCheck = &compareValue{} + +type compareValue struct { + resourceAddresses []string + attributePaths []tfjsonpath.Path + stateValues []any + comparer compare.ValueComparer +} + +func (e *compareValue) AddStateValue(resourceAddress string, attributePath tfjsonpath.Path) StateCheck { + e.resourceAddresses = append(e.resourceAddresses, resourceAddress) + e.attributePaths = append(e.attributePaths, attributePath) + + return e +} + +// CheckState implements the state check logic. +func (e *compareValue) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) { + var resource *tfjson.StateResource + + if req.State == nil { + resp.Error = fmt.Errorf("state is nil") + + return + } + + if req.State.Values == nil { + resp.Error = fmt.Errorf("state does not contain any state values") + + return + } + + if req.State.Values.RootModule == nil { + resp.Error = fmt.Errorf("state does not contain a root module") + + return + } + + // All calls to AddStateValue occur before any TestStep is run, populating the resourceAddresses + // and attributePaths slices. The stateValues slice is populated during execution of each TestStep. + // Each call to CheckState happens sequentially during each TestStep. + // The currentIndex is reflective of the current state value being checked. + currentIndex := len(e.stateValues) + + if len(e.resourceAddresses) <= currentIndex { + resp.Error = fmt.Errorf("resource addresses index out of bounds: %d", currentIndex) + + return + } + + resourceAddress := e.resourceAddresses[currentIndex] + + for _, r := range req.State.Values.RootModule.Resources { + if resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", resourceAddress) + + return + } + + if len(e.attributePaths) <= currentIndex { + resp.Error = fmt.Errorf("attribute paths index out of bounds: %d", currentIndex) + + return + } + + attributePath := e.attributePaths[currentIndex] + + result, err := tfjsonpath.Traverse(resource.AttributeValues, attributePath) + + if err != nil { + resp.Error = err + + return + } + + e.stateValues = append(e.stateValues, result) + + err = e.comparer.CompareValues(e.stateValues...) + + if err != nil { + resp.Error = err + } +} + +// CompareValue returns a state check that compares values retrieved from state using the +// supplied value comparer. +func CompareValue(comparer compare.ValueComparer) *compareValue { + return &compareValue{ + comparer: comparer, + } +} diff --git a/statecheck/compare_value_collection.go b/statecheck/compare_value_collection.go new file mode 100644 index 000000000..7a06c6010 --- /dev/null +++ b/statecheck/compare_value_collection.go @@ -0,0 +1,223 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck + +import ( + "context" + "errors" + "fmt" + "sort" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource State Check +var _ StateCheck = &compareValueCollection{} + +type compareValueCollection struct { + resourceAddressOne string + collectionPath []tfjsonpath.Path + resourceAddressTwo string + attributePath tfjsonpath.Path + comparer compare.ValueComparer +} + +func walkCollectionPath(obj any, paths []tfjsonpath.Path, results []any) ([]any, error) { + switch t := obj.(type) { + case []any: + for _, v := range t { + if len(paths) == 0 { + results = append(results, v) + continue + } + + x, err := tfjsonpath.Traverse(v, paths[0]) + + if err != nil { + return results, err + } + + results, err = walkCollectionPath(x, paths[1:], results) + + if err != nil { + return results, err + } + } + case map[string]any: + keys := make([]string, 0, len(t)) + + for k := range t { + keys = append(keys, k) + } + + sort.Strings(keys) + + for _, key := range keys { + if len(paths) == 0 { + results = append(results, t[key]) + continue + } + + x, err := tfjsonpath.Traverse(t, paths[0]) + + if err != nil { + return results, err + } + + results, err = walkCollectionPath(x, paths[1:], results) + + if err != nil { + return results, err + } + } + default: + results = append(results, obj) + } + + return results, nil +} + +// CheckState implements the state check logic. +func (e *compareValueCollection) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) { + var resourceOne *tfjson.StateResource + var resourceTwo *tfjson.StateResource + + if req.State == nil { + resp.Error = fmt.Errorf("state is nil") + + return + } + + if req.State.Values == nil { + resp.Error = fmt.Errorf("state does not contain any state values") + + return + } + + if req.State.Values.RootModule == nil { + resp.Error = fmt.Errorf("state does not contain a root module") + + return + } + + for _, r := range req.State.Values.RootModule.Resources { + if e.resourceAddressOne == r.Address { + resourceOne = r + + break + } + } + + if resourceOne == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddressOne) + + return + } + + if len(e.collectionPath) == 0 { + resp.Error = fmt.Errorf("%s - No collection path was provided", e.resourceAddressOne) + + return + } + + resultOne, err := tfjsonpath.Traverse(resourceOne.AttributeValues, e.collectionPath[0]) + + if err != nil { + resp.Error = err + + return + } + + // Verify resultOne is a collection. + switch t := resultOne.(type) { + case []any, map[string]any: + // Collection found. + default: + var pathStr string + + for _, v := range e.collectionPath { + pathStr += fmt.Sprintf(".%s", v.String()) + } + + resp.Error = fmt.Errorf("%s%s is not a collection type: %T", e.resourceAddressOne, pathStr, t) + + return + } + + var results []any + + results, err = walkCollectionPath(resultOne, e.collectionPath[1:], results) + + if err != nil { + resp.Error = err + + return + } + + for _, r := range req.State.Values.RootModule.Resources { + if e.resourceAddressTwo == r.Address { + resourceTwo = r + + break + } + } + + if resourceTwo == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddressTwo) + + return + } + + resultTwo, err := tfjsonpath.Traverse(resourceTwo.AttributeValues, e.attributePath) + + if err != nil { + resp.Error = err + + return + } + + var errs []error + + for _, v := range results { + switch resultTwo.(type) { + case []any: + errs = append(errs, e.comparer.CompareValues([]any{v}, resultTwo)) + default: + errs = append(errs, e.comparer.CompareValues(v, resultTwo)) + } + } + + for _, err = range errs { + if err == nil { + return + } + } + + errMsgs := map[string]struct{}{} + + for _, err = range errs { + if _, ok := errMsgs[err.Error()]; ok { + continue + } + + resp.Error = errors.Join(resp.Error, err) + + errMsgs[err.Error()] = struct{}{} + } +} + +// CompareValueCollection returns a state check that iterates over each element in a collection and compares the value of each element +// with the value of an attribute using the given value comparer. +func CompareValueCollection(resourceAddressOne string, collectionPath []tfjsonpath.Path, resourceAddressTwo string, attributePath tfjsonpath.Path, comparer compare.ValueComparer) StateCheck { + return &compareValueCollection{ + resourceAddressOne: resourceAddressOne, + collectionPath: collectionPath, + resourceAddressTwo: resourceAddressTwo, + attributePath: attributePath, + comparer: comparer, + } +} diff --git a/statecheck/compare_value_collection_test.go b/statecheck/compare_value_collection_test.go new file mode 100644 index 000000000..31cdc9ec0 --- /dev/null +++ b/statecheck/compare_value_collection_test.go @@ -0,0 +1,1988 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-plugin-testing/compare" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestCompareValueCollection_CheckState_Bool_Error_NotCollection(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + resource "test_resource" "two" { + bool_attribute = true + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("bool_attribute"), + }, + "test_resource.one", + tfjsonpath.New("bool_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("test_resource.two.bool_attribute is not a collection type: bool"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_Float_Error_NotCollection(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.234 + } + + resource "test_resource" "two" { + float_attribute = 1.234 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("float_attribute"), + }, + "test_resource.one", + tfjsonpath.New("float_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("test_resource.two.float_attribute is not a collection type: json.Number"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_Int_Error_NotCollection(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 1234 + } + + resource "test_resource" "two" { + int_attribute = 1234 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("int_attribute"), + }, + "test_resource.one", + tfjsonpath.New("int_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("test_resource.two.int_attribute is not a collection type: json.Number"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_List_ValuesSame_ErrorDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_attribute = [ + "str2", + "str3", + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str\nexpected values to be the same, but they differ: str3 != str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_EmptyCollectionPath(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_attribute = [ + "str2", + "str", + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + // Empty path is invalid + []tfjsonpath.Path{}, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("test_resource.two - No collection path was provided"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_List_ValuesSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_attribute = [ + "str2", + "str", + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_List_ValuesDiffer_ErrorSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_attribute = [ + "str", + "str", + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + ExpectError: regexp.MustCompile("expected values to differ, but they are the same: str == str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_List_ValuesDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_attribute = [ + "str", + "str2", + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_ListNestedBlock_ValuesSame_ErrorDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_nested_block { + list_nested_block_attribute = "str2" + } + list_nested_block { + list_nested_block_attribute = "str3" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_nested_block"), + tfjsonpath.New("list_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str\nexpected values to be the same, but they differ: str3 != str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_ListNestedBlock_ValuesSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_nested_block { + list_nested_block_attribute = "str2" + } + list_nested_block { + list_nested_block_attribute = "str" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_nested_block"), + tfjsonpath.New("list_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_ListNestedBlock_ValuesDiffer_ErrorSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "str" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_nested_block"), + tfjsonpath.New("list_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + ExpectError: regexp.MustCompile("expected values to differ, but they are the same: str == str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_ListNestedBlock_ValuesDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_nested_block { + list_nested_block_attribute = "str2" + } + list_nested_block { + list_nested_block_attribute = "str3" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_nested_block"), + tfjsonpath.New("list_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_Map_ValuesSame_ErrorDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + map_attribute = { + "a": "str2", + "b": "str3", + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("map_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str\nexpected values to be the same, but they differ: str3 != str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_Map_ValuesSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + map_attribute = { + "a": "str2", + "b": "str", + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("map_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_Map_ValuesDiffer_ErrorSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + map_attribute = { + "a": "str", + "b": "str", + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("map_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + ExpectError: regexp.MustCompile("expected values to differ, but they are the same: str == str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_Map_ValuesDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + map_attribute = { + "a": "str", + "b": "str2", + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("map_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_Set_ValuesSame_ErrorDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + set_attribute = [ + "str2", + "str3" + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str\nexpected values to be the same, but they differ: str3 != str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_Set_ValuesSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + set_attribute = [ + "str2", + "str" + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_Set_ValuesDiffer_ErrorSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + set_attribute = [ + "str", + "str", + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + ExpectError: regexp.MustCompile("expected values to differ, but they are the same: str == str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_Set_ValuesDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + set_attribute = [ + "str", + "str2" + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_SetNestedBlock_ValuesSame_ErrorDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + set_nested_block { + set_nested_block_attribute = "str2" + } + set_nested_block { + set_nested_block_attribute = "str3" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_block"), + tfjsonpath.New("set_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str\nexpected values to be the same, but they differ: str3 != str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_SetNestedBlock_ValuesSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + set_nested_block { + set_nested_block_attribute = "str2" + } + set_nested_block { + set_nested_block_attribute = "str" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_block"), + tfjsonpath.New("set_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_SetNestedBlock_ValuesDiffer_ErrorSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "str" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_block"), + tfjsonpath.New("set_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + ExpectError: regexp.MustCompile("expected values to differ, but they are the same: str == str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_SetNestedBlock_ValuesDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + set_nested_block { + set_nested_block_attribute = "str2" + } + set_nested_block { + set_nested_block_attribute = "str3" + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_block"), + tfjsonpath.New("set_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_SetNestedNestedBlock_ValuesDiffer_ErrorSameAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + tfjsonpath.New("set_nested_block"), + tfjsonpath.New("set_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block_attribute"), + compare.ValuesDiffer(), + ), + }, + ExpectError: regexp.MustCompile("expected values to differ, but they are the same: str == str"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_SetNestedNestedBlock_ValuesDiffer_ErrorSameNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + tfjsonpath.New("set_nested_block"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0), + compare.ValuesDiffer(), + ), + }, + ExpectError: regexp.MustCompile(`expected values to differ, but they are the same: map\[set_nested_block_attribute:str\] == map\[set_nested_block_attribute:str\]`), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_SetNestedNestedBlock_ValuesDiffer_ErrorSameNestedNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block"), + compare.ValuesDiffer(), + ), + }, + ExpectError: regexp.MustCompile(`expected values to differ, but they are the same: \[map\[set_nested_block:\[map\[set_nested_block_attribute:str\]\]\]\] == \[map\[set_nested_block:\[map\[set_nested_block_attribute:str\]\]\]\]`), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_SetNestedNestedBlock_ValuesDifferAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str1" + } + set_nested_block { + set_nested_block_attribute = "str2" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str3" + } + set_nested_block { + set_nested_block_attribute = "str4" + } + } + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str5" + } + set_nested_block { + set_nested_block_attribute = "str6" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + tfjsonpath.New("set_nested_block"), + tfjsonpath.New("set_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block_attribute"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_SetNestedNestedBlock_ValuesDifferNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str1" + } + set_nested_block { + set_nested_block_attribute = "str2" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str3" + } + set_nested_block { + set_nested_block_attribute = "str4" + } + } + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str5" + } + set_nested_block { + set_nested_block_attribute = "str6" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + tfjsonpath.New("set_nested_block"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_SetNestedNestedBlock_ValuesDifferNestedNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str1" + } + set_nested_block { + set_nested_block_attribute = "str2" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str3" + } + set_nested_block { + set_nested_block_attribute = "str4" + } + } + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str5" + } + set_nested_block { + set_nested_block_attribute = "str6" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_SetNested_ValuesSame_ErrorAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_a" + } + set_nested_block { + set_nested_block_attribute = "str_b" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_c" + } + set_nested_block { + set_nested_block_attribute = "str_d" + } + } + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_e" + } + set_nested_block { + set_nested_block_attribute = "str_f" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + tfjsonpath.New("set_nested_block"), + tfjsonpath.New("set_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str_c != str_a\nexpected values to be the same, but they differ: str_d != str_a\nexpected values to be the same, but they differ: str_e != str_a\nexpected values to be the same, but they differ: str_f != str_a"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_SetNested_ValuesSame_ErrorNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_a" + } + set_nested_block { + set_nested_block_attribute = "str_b" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_c" + } + set_nested_block { + set_nested_block_attribute = "str_d" + } + } + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_e" + } + set_nested_block { + set_nested_block_attribute = "str_f" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + tfjsonpath.New("set_nested_block"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile(`expected values to be the same, but they differ: map\[set_nested_block_attribute:str_c\] != map\[set_nested_block_attribute:str_a\]\nexpected values to be the same, but they differ: map\[set_nested_block_attribute:str_d\] != map\[set_nested_block_attribute:str_a\]\nexpected values to be the same, but they differ: map\[set_nested_block_attribute:str_e\] != map\[set_nested_block_attribute:str_a\]\nexpected values to be the same, but they differ: map\[set_nested_block_attribute:str_f\] != map\[set_nested_block_attribute:str_a\]`), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_SetNested_ValuesSame_ErrorNestedNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_a" + } + set_nested_block { + set_nested_block_attribute = "str_b" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_c" + } + set_nested_block { + set_nested_block_attribute = "str_d" + } + } + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_e" + } + set_nested_block { + set_nested_block_attribute = "str_f" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile(`expected values to be the same, but they differ: \[map\[set_nested_block:\[map\[set_nested_block_attribute:str_c\] map\[set_nested_block_attribute:str_d\]\]\]\] != \[map\[set_nested_block:\[map\[set_nested_block_attribute:str_a\] map\[set_nested_block_attribute:str_b\]\]\]\]\nexpected values to be the same, but they differ: \[map\[set_nested_block:\[map\[set_nested_block_attribute:str_e\] map\[set_nested_block_attribute:str_f\]\]\]\] != \[map\[set_nested_block:\[map\[set_nested_block_attribute:str_a\] map\[set_nested_block_attribute:str_b\]\]\]\]`), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_SetNested_ValuesSameAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_a" + } + set_nested_block { + set_nested_block_attribute = "str_b" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_c" + } + set_nested_block { + set_nested_block_attribute = "str_d" + } + } + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_a" + } + set_nested_block { + set_nested_block_attribute = "str_b" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + tfjsonpath.New("set_nested_block"), + tfjsonpath.New("set_nested_block_attribute"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_SetNested_ValuesSameNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_a" + } + set_nested_block { + set_nested_block_attribute = "str_b" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_c" + } + set_nested_block { + set_nested_block_attribute = "str_d" + } + } + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_a" + } + set_nested_block { + set_nested_block_attribute = "str_b" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + tfjsonpath.New("set_nested_block"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block").AtSliceIndex(0).AtMapKey("set_nested_block").AtSliceIndex(0), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_SetNested_ValuesSameNestedNestedBlock(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_a" + } + set_nested_block { + set_nested_block_attribute = "str_b" + } + } + } + + resource "test_resource" "two" { + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_c" + } + set_nested_block { + set_nested_block_attribute = "str_d" + } + } + set_nested_nested_block { + set_nested_block { + set_nested_block_attribute = "str_a" + } + set_nested_block { + set_nested_block_attribute = "str_b" + } + } + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("set_nested_nested_block"), + }, + "test_resource.one", + tfjsonpath.New("set_nested_nested_block"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_String_Error_NotCollection(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + string_attribute = "str" + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("string_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("test_resource.two.string_attribute is not a collection type: string"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_ListNestedAttribute_ValuesSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // Nested attributes only available in protocol version 6 + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "str_attr", + Type: tftypes.String, + Optional: true, + }, + { + Name: "nested_attr", + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "str_attr", + Type: tftypes.String, + Optional: true, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeList, + }, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + str_attr = "str2" + } + resource "test_resource" "two" { + nested_attr = [ + { + str_attr = "str1" + }, + { + str_attr = "str2" + } + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("nested_attr"), + tfjsonpath.New("str_attr"), + }, + "test_resource.one", + tfjsonpath.New("str_attr"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_ListNestedAttribute_ValuesSame_ErrorDiff(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // Nested attributes only available in protocol version 6 + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "str_attr", + Type: tftypes.String, + Optional: true, + }, + { + Name: "nested_attr", + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "str_attr", + Type: tftypes.String, + Optional: true, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeList, + }, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + str_attr = "str1" + } + resource "test_resource" "two" { + nested_attr = [ + { + str_attr = "str2" + }, + { + str_attr = "str3" + } + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("nested_attr"), + tfjsonpath.New("str_attr"), + }, + "test_resource.one", + tfjsonpath.New("str_attr"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str1\nexpected values to be the same, but they differ: str3 != str1"), + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_DoubleListNestedAttribute_ValuesSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // Nested attributes only available in protocol version 6 + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "str_attr", + Type: tftypes.String, + Optional: true, + }, + { + Name: "nested_attr", + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "double_nested_attr", + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "str_attr", + Type: tftypes.String, + Optional: true, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeSingle, + }, + Optional: true, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeList, + }, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + str_attr = "str2" + } + resource "test_resource" "two" { + nested_attr = [ + { + double_nested_attr = { + str_attr = "str1" + } + }, + { + double_nested_attr = { + str_attr = "str2" + } + } + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("nested_attr"), + tfjsonpath.New("double_nested_attr"), + tfjsonpath.New("str_attr"), + }, + "test_resource.one", + tfjsonpath.New("str_attr"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValueCollection_CheckState_DoubleListNestedAttribute_ValuesSame_ErrorDiff(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // Nested attributes only available in protocol version 6 + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "str_attr", + Type: tftypes.String, + Optional: true, + }, + { + Name: "nested_attr", + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "double_nested_attr", + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "str_attr", + Type: tftypes.String, + Optional: true, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeSingle, + }, + Optional: true, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeList, + }, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + str_attr = "str1" + } + resource "test_resource" "two" { + nested_attr = [ + { + double_nested_attr = { + str_attr = "str2" + } + }, + { + double_nested_attr = { + str_attr = "str3" + } + } + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("nested_attr"), + tfjsonpath.New("double_nested_attr"), + tfjsonpath.New("str_attr"), + }, + "test_resource.one", + tfjsonpath.New("str_attr"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("expected values to be the same, but they differ: str2 != str1\nexpected values to be the same, but they differ: str3 != str1"), + }, + }, + }) +} diff --git a/statecheck/compare_value_pairs.go b/statecheck/compare_value_pairs.go new file mode 100644 index 000000000..8db67c562 --- /dev/null +++ b/statecheck/compare_value_pairs.go @@ -0,0 +1,111 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck + +import ( + "context" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource State Check +var _ StateCheck = &compareValuePairs{} + +type compareValuePairs struct { + resourceAddressOne string + attributePathOne tfjsonpath.Path + resourceAddressTwo string + attributePathTwo tfjsonpath.Path + comparer compare.ValueComparer +} + +// CheckState implements the state check logic. +func (e *compareValuePairs) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) { + var resourceOne *tfjson.StateResource + var resourceTwo *tfjson.StateResource + + if req.State == nil { + resp.Error = fmt.Errorf("state is nil") + + return + } + + if req.State.Values == nil { + resp.Error = fmt.Errorf("state does not contain any state values") + + return + } + + if req.State.Values.RootModule == nil { + resp.Error = fmt.Errorf("state does not contain a root module") + + return + } + + for _, r := range req.State.Values.RootModule.Resources { + if e.resourceAddressOne == r.Address { + resourceOne = r + + break + } + } + + if resourceOne == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddressOne) + + return + } + + resultOne, err := tfjsonpath.Traverse(resourceOne.AttributeValues, e.attributePathOne) + + if err != nil { + resp.Error = err + + return + } + + for _, r := range req.State.Values.RootModule.Resources { + if e.resourceAddressTwo == r.Address { + resourceTwo = r + + break + } + } + + if resourceTwo == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddressTwo) + + return + } + + resultTwo, err := tfjsonpath.Traverse(resourceTwo.AttributeValues, e.attributePathTwo) + + if err != nil { + resp.Error = err + + return + } + + err = e.comparer.CompareValues(resultOne, resultTwo) + + if err != nil { + resp.Error = err + } +} + +// CompareValuePairs returns a state check that compares the value in state for the first given resource address and +// path with the value in state for the second given resource address and path using the supplied value comparer. +func CompareValuePairs(resourceAddressOne string, attributePathOne tfjsonpath.Path, resourceAddressTwo string, attributePathTwo tfjsonpath.Path, comparer compare.ValueComparer) StateCheck { + return &compareValuePairs{ + resourceAddressOne: resourceAddressOne, + attributePathOne: attributePathOne, + resourceAddressTwo: resourceAddressTwo, + attributePathTwo: attributePathTwo, + comparer: comparer, + } +} diff --git a/statecheck/compare_value_pairs_test.go b/statecheck/compare_value_pairs_test.go new file mode 100644 index 000000000..4df478832 --- /dev/null +++ b/statecheck/compare_value_pairs_test.go @@ -0,0 +1,142 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-plugin-testing/compare" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestCompareValuePairs_CheckState_ValuesSame_DifferError(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + float_attribute = 1.234 + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + "test_resource.one", + tfjsonpath.New("float_attribute"), + compare.ValuesSame(), + ), + }, + ExpectError: regexp.MustCompile("expected values to be the same, but they differ: true != 1.234"), + }, + }, + }) +} + +func TestCompareValuePairs_CheckState_ValuesSame(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + resource "test_resource" "two" { + bool_attribute = true + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + "test_resource.two", + tfjsonpath.New("bool_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} + +func TestCompareValuePairs_CheckState_ValuesDiffer_SameError(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + resource "test_resource" "two" { + bool_attribute = true + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + "test_resource.two", + tfjsonpath.New("bool_attribute"), + compare.ValuesDiffer(), + ), + }, + ExpectError: regexp.MustCompile("expected values to differ, but they are the same: true == true"), + }, + }, + }) +} + +func TestCompareValuePairs_CheckState_ValuesDiffer(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + float_attribute = 1.234 + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + "test_resource.one", + tfjsonpath.New("float_attribute"), + compare.ValuesDiffer(), + ), + }, + }, + }, + }) +} diff --git a/statecheck/compare_value_test.go b/statecheck/compare_value_test.go new file mode 100644 index 000000000..271692425 --- /dev/null +++ b/statecheck/compare_value_test.go @@ -0,0 +1,241 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-plugin-testing/compare" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestCompareValue_CheckState_NoStateValues(t *testing.T) { + t.Parallel() + + boolValuesDiffer := statecheck.CompareValue(compare.ValuesSame()) + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + // No state values have been added + boolValuesDiffer, + }, + ExpectError: regexp.MustCompile(`resource addresses index out of bounds: 0`), + }, + }, + }) +} + +func TestCompareValue_CheckState_ValuesSame_ValueDiffersError(t *testing.T) { + t.Parallel() + + boolValuesDiffer := statecheck.CompareValue(compare.ValuesSame()) + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = false + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + ExpectError: regexp.MustCompile(`expected values to be the same, but they differ: true != false`), + }, + }, + }) +} + +func TestCompareValue_CheckState_ValuesSame(t *testing.T) { + t.Parallel() + + boolValuesDiffer := statecheck.CompareValue(compare.ValuesSame()) + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + }, + }) +} + +func TestCompareValue_CheckState_ValuesDiffer_ValueSameError(t *testing.T) { + t.Parallel() + + boolValuesDiffer := statecheck.CompareValue(compare.ValuesDiffer()) + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = false + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = false + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + ExpectError: regexp.MustCompile(`expected values to differ, but they are the same: false == false`), + }, + }, + }) +} + +func TestCompareValue_CheckState_ValuesDiffer(t *testing.T) { + t.Parallel() + + boolValuesDiffer := statecheck.CompareValue(compare.ValuesDiffer()) + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = false + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + boolValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + ), + }, + }, + }, + }) +} diff --git a/statecheck/expect_known_value_test.go b/statecheck/expect_known_value_test.go index f02b8cd09..2eb56d4e4 100644 --- a/statecheck/expect_known_value_test.go +++ b/statecheck/expect_known_value_test.go @@ -1622,6 +1622,26 @@ func testProvider() *schema.Provider { }, }, }, + "set_nested_nested_block": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "set_nested_block": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "set_nested_block_attribute": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, "string_attribute": { Optional: true, Type: schema.TypeString, diff --git a/website/data/plugin-testing-nav-data.json b/website/data/plugin-testing-nav-data.json index 6fbce82b3..be9cff643 100644 --- a/website/data/plugin-testing-nav-data.json +++ b/website/data/plugin-testing-nav-data.json @@ -140,6 +140,15 @@ } ] }, + { + "title": "Value Comparers", + "routes": [ + { + "title": "Overview", + "path": "acceptance-tests/value-comparers" + } + ] + }, { "title": "Sweepers", "path": "acceptance-tests/sweepers" diff --git a/website/docs/plugin/testing/acceptance-tests/known-value-checks/index.mdx b/website/docs/plugin/testing/acceptance-tests/known-value-checks/index.mdx index a942ce660..569f6b5b6 100644 --- a/website/docs/plugin/testing/acceptance-tests/known-value-checks/index.mdx +++ b/website/docs/plugin/testing/acceptance-tests/known-value-checks/index.mdx @@ -2,18 +2,19 @@ page_title: 'Plugin Development - Acceptance Testing: Known Values' description: >- How to use known values in the testing module. - Known values define an expected type, and value for a resource attribute, or output value in a Terraform plan for use in Plan Checks. + Known values define an expected type, and value for a resource attribute, or output value in a Terraform plan or state for use in Plan Checks or State Checks. --- # Known Value Checks -Known Value Checks are for use in conjunction with [Plan Checks](/terraform/plugin/testing/acceptance-tests/plan-checks), which leverage the [terraform-json](https://pkg.go.dev/github.com/hashicorp/terraform-json) representation of a Terraform plan. +Known Value Checks are for use in conjunction with [Plan Checks](/terraform/plugin/testing/acceptance-tests/plan-checks), and [State Checks](/terraform/plugin/testing/acceptance-tests/state-checks) which leverage the [terraform-json](https://pkg.go.dev/github.com/hashicorp/terraform-json) representation of a Terraform plan. ## Usage Example uses in the testing module include: -- The [`ExpectknownValue()`](/terraform/plugin/testing/acceptance-tests/plan-checks/resource#example-using-plancheck-expectknownvalue), [`ExpectKnownOutputValue()`](/terraform/plugin/testing/acceptance-tests/plan-checks/output#example-using-plancheck-expectknownoutputvalue) and [`ExpectKnownOutputValueAtPath()`](/terraform/plugin/testing/acceptance-tests/plan-checks/output#example-using-plancheck-expectknownoutputvalueatpath) [built-in plan checks](/terraform/plugin/testing/acceptance-tests/plan-checks) use known value checks for asserting whether a specific resource attribute, or output value has a particular type, and value. +- **Plan Checks**: The [`ExpectKnownValue()`](/terraform/plugin/testing/acceptance-tests/plan-checks/resource#expectknownvalue-plan-check), [`ExpectKnownOutputValue()`](/terraform/plugin/testing/acceptance-tests/plan-checks/output#expectknownoutputvalue-plan-check) and [`ExpectKnownOutputValueAtPath()`](/terraform/plugin/testing/acceptance-tests/plan-checks/output#expectknownoutputvalueatpath-plan-check) [built-in plan checks](/terraform/plugin/testing/acceptance-tests/plan-checks) use known value checks for asserting whether a specific resource attribute, or output value has a particular type, and value. +- **State Checks**: The [`ExpectKnownValue()`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#expectknownvalue-state-check), [`ExpectKnownOutputValue()`](/terraform/plugin/testing/acceptance-tests/state-checks/output#expectknownoutputvalue-state-check) and [`ExpectKnownOutputValueAtPath()`](/terraform/plugin/testing/acceptance-tests/state-checks/output#expectknownoutputvalueatpath-state-check) [built-in state checks](/terraform/plugin/testing/acceptance-tests/state-checks) use known value checks for asserting whether a specific resource attribute, or output value has a particular type, and value. ## Using a Known Value Check diff --git a/website/docs/plugin/testing/acceptance-tests/state-checks/resource.mdx b/website/docs/plugin/testing/acceptance-tests/state-checks/resource.mdx index a3e94cbbe..d11db8f63 100644 --- a/website/docs/plugin/testing/acceptance-tests/state-checks/resource.mdx +++ b/website/docs/plugin/testing/acceptance-tests/state-checks/resource.mdx @@ -9,10 +9,222 @@ description: >- The `terraform-plugin-testing` module provides a package [`statecheck`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/statecheck) with built-in managed resource, and data source state checks for common use-cases: -| Check | Description | -|-----------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [`ExpectKnownValue`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#expectknownvalue-state-check) | Asserts the specified attribute at the given managed resource, or data source, has the specified type, and value. | -| [`ExpectSensitiveValue`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#expectsensitivevalue-state-check) | Asserts the specified attribute at the given managed resource, or data source, has a sensitive value. | +| Check | Description | +|---------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [`CompareValue`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#comparevalue-state-check) | Compares sequential values of the specified attribute at the given managed resource, or data source, using the supplied [value comparer](/terraform/plugin/testing/acceptance-tests/value-comparers). | +| [`CompareValueCollection`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#comparevaluecollection-state-check) | Compares each item in the specified collection (e.g., list, set) attribute, with the second specified attribute at the given managed resources, or data sources, using the supplied [value comparer](/terraform/plugin/testing/acceptance-tests/value-comparers). | +| [`CompareValuePairs`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#comparevaluecollection-state-check) | Compares the specified attributes at the given managed resources, or data sources, using the supplied [value comparer](/terraform/plugin/testing/acceptance-tests/value-comparers). | +| [`ExpectKnownValue`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#expectknownvalue-state-check) | Asserts the specified attribute at the given managed resource, or data source, has the specified type, and value. | +| [`ExpectSensitiveValue`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#expectsensitivevalue-state-check) | Asserts the specified attribute at the given managed resource, or data source, has a sensitive value. | + +## `CompareValue` State Check + +The intended usage of [`statecheck.CompareValue(comparer)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/statecheck#CompareValue) state check is to retrieve a specific resource attribute value from state during sequential test steps, and to compare these values using the supplied value comparer. + +Refer to [Value Comparers](/terraform/plugin/testing/acceptance-tests/value-comparers) for details, and examples of the available [compare.ValueComparer](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/compare#ValueComparer) types that can be used with the `CompareValue` state check. + +```go +package example_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestCompareValue_CheckState_ValuesSame(t *testing.T) { + t.Parallel() + + compareValuesSame := statecheck.CompareValue(compare.ValuesSame()) + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + // Example resource containing a computed attribute named "computed_attribute" + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + compareValuesSame.AddStateValue( + "test_resource.one", + tfjsonpath.New("computed_attribute"), + ), + }, + }, + { + // Example resource containing a computed attribute named "computed_attribute" + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + compareValuesSame.AddStateValue( + "test_resource.one", + tfjsonpath.New("computed_attribute"), + ), + }, + }, + }, + }) +} +``` + +## `CompareValueCollection` State Check + +The [`statecheck.CompareValueCollection(resourceAddressOne, collectionPath, resourceAddressTwo, attributePath, comparer)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/statecheck#CompareValueCollection) state check retrieves a specific collection (e.g., list, set) resource attribute, and a second resource attribute from state, and compares each of the items in the collection with the second attribute using the supplied value comparer. + +Refer to [Value Comparers](/terraform/plugin/testing/acceptance-tests/value-comparers) for details, and examples of the available [compare.ValueComparer](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/compare#ValueComparer) types that can be used with the `CompareValueCollection` state check. + +The following example illustrates how a `CompareValue` state check can be used to determine whether an attribute value appears in a collection attribute. Note that this is for illustrative purposes only, `CompareValue` should only be used for checking computed values. + +```go +package example_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestCompareValueCollection_CheckState_ValuesSame(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + // The following is for illustrative purposes. State checking + // should only be used for computed attributes + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + resource "test_resource" "two" { + list_attribute = [ + "str2", + "str", + ] + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.two", + []tfjsonpath.Path{ + tfjsonpath.New("list_attribute"), + }, + "test_resource.one", + tfjsonpath.New("string_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} +``` + +The following example illustrates how a `CompareValue` state check can be used to determine whether an object attribute value appears in a collection (e.g., list) attribute containing objects. Note that this is for illustrative purposes only, `CompareValue` should only be used for checking computed values. + + +```go +package example_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestCompareValueCollection_CheckState_ValuesSame(t *testing.T) { + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + // The following is for illustrative purposes. State checking + // should only be used for computed attributes + Config: `resource "test_resource" "one" { + list_nested_attribute = [ + { + a = false + b = "two" + }, + { + a = true + b = "four" + } + ] + single_nested_attribute = { + a = true + b = "four" + } + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValueCollection( + "test_resource.one", + []tfjsonpath.Path{ + tfjsonpath.New("list_nested_attribute"), + tfjsonpath.New("b"), + }, + "test_resource.one", + tfjsonpath.New("single_nested_attribute").AtMapKey("b"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} +``` + +## `CompareValuePairs` State Check + +The [`statecheck.CompareValuePairs(resourceAddressOne, attributePathOne, resourceAddressTwo, attributePathTwo, comparer)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/statecheck#CompareValuePairs) state check provides a basis for retrieving a pair of attribute values, and comparing them using the supplied value comparer. + +Refer to [Value Comparers](/terraform/plugin/testing/acceptance-tests/value-comparers) for details, and examples of the available [compare.ValueComparer](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/compare#ValueComparer) types that can be used with the `CompareValuePairs` state check. + +```go +package statecheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestCompareValuePairs_CheckState_ValuesSame(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + // Example resource containing a computed attribute named "computed_attribute" + Config: `resource "test_resource" "one" {} + + resource "test_resource" "two" {} + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs( + "test_resource.one", + tfjsonpath.New("computed_attribute"), + "test_resource.two", + tfjsonpath.New("computed_attribute"), + compare.ValuesSame(), + ), + }, + }, + }, + }) +} +``` ## `ExpectKnownValue` State Check diff --git a/website/docs/plugin/testing/acceptance-tests/testcase.mdx b/website/docs/plugin/testing/acceptance-tests/testcase.mdx index 57ddd888f..77ba53078 100644 --- a/website/docs/plugin/testing/acceptance-tests/testcase.mdx +++ b/website/docs/plugin/testing/acceptance-tests/testcase.mdx @@ -157,7 +157,7 @@ but before any test steps are executed. The [Terraform Version Checks](/terrafor are generic checks that check logic against the Terraform CLI version and can immediately pass or fail a test before any test steps are executed. -The tfversion package provides built-in checks for common scenarios. +The [`tfversion`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion) package provides built-in checks for common scenarios. **Example usage:** diff --git a/website/docs/plugin/testing/acceptance-tests/value-comparers/index.mdx b/website/docs/plugin/testing/acceptance-tests/value-comparers/index.mdx new file mode 100644 index 000000000..3ef5e19d6 --- /dev/null +++ b/website/docs/plugin/testing/acceptance-tests/value-comparers/index.mdx @@ -0,0 +1,183 @@ +--- +page_title: 'Plugin Development - Acceptance Testing: Value Comparers' +description: >- + How to use value comparers in the testing module. + Value comparers define a comparison for a resource attribute, or output value for use in State Checks. +--- + +# Value Comparers + + + +Value Comparers are for use in conjunction with [State Checks](/terraform/plugin/testing/acceptance-tests/state-checks), which leverage the [terraform-json](https://pkg.go.dev/github.com/hashicorp/terraform-json) representation of Terraform state. + + + +Value comparers can be used to assert a resource or data source attribute value across multiple [Test Steps](/terraform/plugin/testing/acceptance-tests/teststep), like asserting that a randomly generated resource attribute doesn't change after multiple apply steps. This is done by creating the value comparer, typically before the test case is defined, using the relevant constructor function: +```go +func TestExample(t *testing.T) { + // Create the value comparer so we can add state values to it during the test steps + compareValuesDiffer := statecheck.CompareValue(compare.ValuesDiffer()) + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + // .. test steps omitted + }, + }) +} +``` + +Once the value comparer is created, state values can be added in `TestStep.ConfigStateChecks`: +```go +func TestExample(t *testing.T) { + // Create the value comparer so we can add state values to it during the test steps + compareValuesDiffer := statecheck.CompareValue(compare.ValuesDiffer()) + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + // Add the current state value of "computed_attribute" to the value comparer. + // Since there are no other state values at this point, no assertion is made. + compareValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("computed_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + // Add the current state value of "computed_attribute" to the value comparer. + // Since there is an existing state value in the value comparer at this point, + // if the two values are equal, the test will produce an error. + compareValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("computed_attribute"), + ), + }, + }, + }, + }) +} +``` + +The value comparer implementation (defined by the [`ValueComparer` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/compare#ValueComparer)) determines what assertion occurs when a state value is added. The built-in value comparers are: +- [`CompareValue()`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#comparevalue-state-check) +- [`CompareValueCollection()`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#comparevaluecollection-state-check) +- [`CompareValuePairs()`](/terraform/plugin/testing/acceptance-tests/state-checks/resource#comparevaluepairs-state-check) + +## Values Differ + +The [ValuesDiffer](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/compare#ValuesDiffer) value comparer verifies that each value in the sequence of values supplied to the `CompareValues()` method differs from the preceding value. + +Example usage of [ValuesDiffer](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/compare#ValuesDiffer) in a [CompareValue](/terraform/plugin/testing/acceptance-tests/state-checks/resource#comparevalue-state-check) state check. + +```go +package example_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestCompareValue_CheckState_ValuesDiffer(t *testing.T) { + t.Parallel() + + compareValuesDiffer := statecheck.CompareValue(compare.ValuesDiffer()) + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + // Add the current state value of "computed_attribute" to the value comparer. + // Since there are no other state values at this point, no assertion is made. + compareValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("computed_attribute"), + ), + }, + }, + { + // Add the current state value of "computed_attribute" to the value comparer. + // Since there is an existing state value in the value comparer at this point, + // if the two values are equal, the test will produce an error. + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + compareValuesDiffer.AddStateValue( + "test_resource.one", + tfjsonpath.New("computed_attribute"), + ), + }, + }, + }, + }) +} +``` + +## Values Same + +The [ValuesSame](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/compare#ValuesSame) value comparer verifies that each value in the sequence of values supplied to the `CompareValues()` method is the same as the preceding value. + +Example usage of [ValuesSame](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/compare#ValuesSame) in a [CompareValue](/terraform/plugin/testing/acceptance-tests/state-checks/resource#comparevalue-state-check) state check. + +```go +package example_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestCompareValue_CheckState_ValuesSame(t *testing.T) { + t.Parallel() + + compareValuesSame := statecheck.CompareValue(compare.ValuesSame()) + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + // Add the current state value of "computed_attribute" to the value comparer. + // Since there are no other state values at this point, no assertion is made. + compareValuesSame.AddStateValue( + "test_resource.one", + tfjsonpath.New("computed_attribute"), + ), + }, + }, + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + // Add the current state value of "computed_attribute" to the value comparer. + // Since there is an existing state value in the value comparer at this point, + // if the two values are not equal, the test will produce an error. + compareValuesSame.AddStateValue( + "test_resource.one", + tfjsonpath.New("computed_attribute"), + ), + }, + }, + }, + }) +} +``` diff --git a/website/docs/plugin/testing/index.mdx b/website/docs/plugin/testing/index.mdx index 2ed9cb366..d994c3ad7 100644 --- a/website/docs/plugin/testing/index.mdx +++ b/website/docs/plugin/testing/index.mdx @@ -37,10 +37,6 @@ plugins, **it is highly recommended to use an account dedicated to testing, to ensure no infrastructure is created in error in any environment that cannot be completely and safely destroyed.** -HashiCorp runs nightly acceptance tests of providers found in the [Terraform -Providers GitHub Organization](https://github.com/terraform-providers) to ensure -each Provider is working correctly. - For a given plugin, Acceptance Tests can be run from the root of the project by using a common make task: