diff --git a/.changes/unreleased/FEATURES-20231218-114539.yaml b/.changes/unreleased/FEATURES-20231218-114539.yaml new file mode 100644 index 000000000..97567a488 --- /dev/null +++ b/.changes/unreleased/FEATURES-20231218-114539.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'plancheck: Added `ExpectKnownValue` plan check, which asserts that a given + resource attribute has a defined type, and value' +time: 2023-12-18T11:45:39.181954Z +custom: + Issue: "248" diff --git a/.changes/unreleased/FEATURES-20231218-114553.yaml b/.changes/unreleased/FEATURES-20231218-114553.yaml new file mode 100644 index 000000000..3627c9eee --- /dev/null +++ b/.changes/unreleased/FEATURES-20231218-114553.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'plancheck: Added `ExpectKnownOutputValue` plan check, which asserts that a + given output value has a defined type, and value' +time: 2023-12-18T11:45:53.272412Z +custom: + Issue: "248" diff --git a/.changes/unreleased/FEATURES-20231218-114611.yaml b/.changes/unreleased/FEATURES-20231218-114611.yaml new file mode 100644 index 000000000..09fe1d9dc --- /dev/null +++ b/.changes/unreleased/FEATURES-20231218-114611.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'plancheck: Added `ExpectKnownOutputValueAtPath` plan check, which asserts that + a given output value at a specified path has a defined type, and value' +time: 2023-12-18T11:46:11.58053Z +custom: + Issue: "248" diff --git a/.changes/unreleased/FEATURES-20231218-114739.yaml b/.changes/unreleased/FEATURES-20231218-114739.yaml new file mode 100644 index 000000000..85498d64a --- /dev/null +++ b/.changes/unreleased/FEATURES-20231218-114739.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'knownvalue: Introduced new `knownvalue` package which contains types for working + with plan checks and state checks' +time: 2023-12-18T11:47:39.059813Z +custom: + Issue: "248" diff --git a/.changes/unreleased/NOTES-20240104-083841.yaml b/.changes/unreleased/NOTES-20240104-083841.yaml new file mode 100644 index 000000000..e281b21b6 --- /dev/null +++ b/.changes/unreleased/NOTES-20240104-083841.yaml @@ -0,0 +1,6 @@ +kind: NOTES +body: Numerical values in the plan are now represented as json.Number, not float64. + Custom plan checks relying upon float64 representation may need altering +time: 2024-01-04T08:38:41.645745Z +custom: + Issue: "248" diff --git a/go.mod b/go.mod index af603705e..a0e86eb12 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/hashicorp/hc-install v0.6.2 github.com/hashicorp/hcl/v2 v2.19.1 github.com/hashicorp/logutils v1.0.0 - github.com/hashicorp/terraform-exec v0.19.0 + github.com/hashicorp/terraform-exec v0.20.0 github.com/hashicorp/terraform-json v0.20.0 github.com/hashicorp/terraform-plugin-go v0.20.0 github.com/hashicorp/terraform-plugin-log v0.9.0 diff --git a/go.sum b/go.sum index 75be7ff7e..09f32d27c 100644 --- a/go.sum +++ b/go.sum @@ -59,8 +59,8 @@ github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5R github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.19.0 h1:FpqZ6n50Tk95mItTSS9BjeOVUb4eg81SpgVtZNNtFSM= -github.com/hashicorp/terraform-exec v0.19.0/go.mod h1:tbxUpe3JKruE9Cuf65mycSIT8KiNPZ0FkuTE3H4urQg= +github.com/hashicorp/terraform-exec v0.20.0 h1:DIZnPsqzPGuUnq6cH8jWcPunBfY+C+M8JyYF3vpnuEo= +github.com/hashicorp/terraform-exec v0.20.0/go.mod h1:ckKGkJWbsNqFKV1itgMnE0hY9IYf1HoiekpuN0eWoDw= github.com/hashicorp/terraform-json v0.20.0 h1:cJcvn4gIOTi0SD7pIy+xiofV1zFA3hza+6K+fo52IX8= github.com/hashicorp/terraform-json v0.20.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk= github.com/hashicorp/terraform-plugin-go v0.20.0 h1:oqvoUlL+2EUbKNsJbIt3zqqZ7wi6lzn4ufkn/UA51xQ= diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 95d6b1af8..2c8888d72 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -305,7 +305,7 @@ func (wd *WorkingDir) SavedPlan(ctx context.Context) (*tfjson.Plan, error) { logging.HelperResourceTrace(ctx, "Calling Terraform CLI show command for JSON plan") - plan, err := wd.tf.ShowPlanFile(context.Background(), wd.planFilename(), tfexec.Reattach(wd.reattachInfo)) + plan, err := wd.tf.ShowPlanFile(context.Background(), wd.planFilename(), tfexec.Reattach(wd.reattachInfo), tfexec.JSONNumber(true)) logging.HelperResourceTrace(ctx, "Calling Terraform CLI show command for JSON plan") diff --git a/knownvalue/bool.go b/knownvalue/bool.go new file mode 100644 index 000000000..62521f454 --- /dev/null +++ b/knownvalue/bool.go @@ -0,0 +1,44 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" + "strconv" +) + +var _ Check = boolExact{} + +type boolExact struct { + value bool +} + +// CheckValue determines whether the passed value is of type bool, and +// contains a matching bool value. +func (v boolExact) CheckValue(other any) error { + otherVal, ok := other.(bool) + + if !ok { + return fmt.Errorf("expected bool value for BoolExact check, got: %T", other) + } + + if otherVal != v.value { + return fmt.Errorf("expected value %t for BoolExact check, got: %t", v.value, otherVal) + } + + return nil +} + +// String returns the string representation of the bool value. +func (v boolExact) String() string { + return strconv.FormatBool(v.value) +} + +// BoolExact returns a Check for asserting equality between the +// supplied bool and the value passed to the CheckValue method. +func BoolExact(value bool) boolExact { + return boolExact{ + value: value, + } +} diff --git a/knownvalue/bool_test.go b/knownvalue/bool_test.go new file mode 100644 index 000000000..3c47d59a9 --- /dev/null +++ b/knownvalue/bool_test.go @@ -0,0 +1,83 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestBoolValue_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.BoolExact(false), + expectedError: fmt.Errorf("expected bool value for BoolExact check, got: "), + }, + "zero-other": { + self: knownvalue.BoolExact(false), + other: false, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.BoolExact(false), + expectedError: fmt.Errorf("expected bool value for BoolExact check, got: "), + }, + "wrong-type": { + self: knownvalue.BoolExact(true), + other: 1.23, + expectedError: fmt.Errorf("expected bool value for BoolExact check, got: float64"), + }, + "not-equal": { + self: knownvalue.BoolExact(true), + other: false, + expectedError: fmt.Errorf("expected value true for BoolExact check, got: false"), + }, + "equal": { + self: knownvalue.BoolExact(true), + other: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolValue_String(t *testing.T) { + t.Parallel() + + got := knownvalue.BoolExact(true).String() + + if diff := cmp.Diff(got, "true"); 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/knownvalue/check.go b/knownvalue/check.go new file mode 100644 index 000000000..cef532c94 --- /dev/null +++ b/knownvalue/check.go @@ -0,0 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +// Check defines an interface that is implemented to determine whether type and value match. Individual +// implementations determine how the match is performed (e.g., exact match, partial match). +type Check interface { + // CheckValue should assert the given known value against any expectations. Use the error + // return to signal unexpected values or implementation errors. + CheckValue(value any) error + // String should return a string representation of the type and value. + String() string +} diff --git a/knownvalue/doc.go b/knownvalue/doc.go new file mode 100644 index 000000000..4041c3e93 --- /dev/null +++ b/knownvalue/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package knownvalue contains the known value interface, and types implementing the known value interface. +package knownvalue diff --git a/knownvalue/float64.go b/knownvalue/float64.go new file mode 100644 index 000000000..bacdaa6fa --- /dev/null +++ b/knownvalue/float64.go @@ -0,0 +1,51 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "encoding/json" + "fmt" + "strconv" +) + +var _ Check = float64Exact{} + +type float64Exact struct { + value float64 +} + +// CheckValue determines whether the passed value is of type float64, and +// contains a matching float64 value. +func (v float64Exact) CheckValue(other any) error { + jsonNum, ok := other.(json.Number) + + if !ok { + return fmt.Errorf("expected json.Number value for Float64Exact check, got: %T", other) + } + + otherVal, err := jsonNum.Float64() + + if err != nil { + return fmt.Errorf("expected json.Number to be parseable as float64 value for Float64Exact check: %s", err) + } + + if otherVal != v.value { + return fmt.Errorf("expected value %s for Float64Exact check, got: %s", v.String(), strconv.FormatFloat(otherVal, 'f', -1, 64)) + } + + return nil +} + +// String returns the string representation of the float64 value. +func (v float64Exact) String() string { + return strconv.FormatFloat(v.value, 'f', -1, 64) +} + +// Float64Exact returns a Check for asserting equality between the +// supplied float64 and the value passed to the CheckValue method. +func Float64Exact(value float64) float64Exact { + return float64Exact{ + value: value, + } +} diff --git a/knownvalue/float64_test.go b/knownvalue/float64_test.go new file mode 100644 index 000000000..2a051f8b2 --- /dev/null +++ b/knownvalue/float64_test.go @@ -0,0 +1,75 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestFloat64Value_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.Float64Exact(0), + expectedError: fmt.Errorf("expected json.Number value for Float64Exact check, got: "), + }, + "zero-other": { + self: knownvalue.Float64Exact(0), + other: json.Number("0.0"), // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.Float64Exact(1.234), + expectedError: fmt.Errorf("expected json.Number value for Float64Exact check, got: "), + }, + "wrong-type": { + self: knownvalue.Float64Exact(1.234), + other: json.Number("str"), + expectedError: fmt.Errorf("expected json.Number to be parseable as float64 value for Float64Exact check: strconv.ParseFloat: parsing \"str\": invalid syntax"), + }, + "not-equal": { + self: knownvalue.Float64Exact(1.234), + other: json.Number("4.321"), + expectedError: fmt.Errorf("expected value 1.234 for Float64Exact check, got: 4.321"), + }, + "equal": { + self: knownvalue.Float64Exact(1.234), + other: json.Number("1.234"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64Value_String(t *testing.T) { + t.Parallel() + + got := knownvalue.Float64Exact(1.234567890123e+09).String() + + if diff := cmp.Diff(got, "1234567890.123"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/int64.go b/knownvalue/int64.go new file mode 100644 index 000000000..19f803622 --- /dev/null +++ b/knownvalue/int64.go @@ -0,0 +1,51 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "encoding/json" + "fmt" + "strconv" +) + +var _ Check = int64Exact{} + +type int64Exact struct { + value int64 +} + +// CheckValue determines whether the passed value is of type int64, and +// contains a matching int64 value. +func (v int64Exact) CheckValue(other any) error { + jsonNum, ok := other.(json.Number) + + if !ok { + return fmt.Errorf("expected json.Number value for Int64Exact check, got: %T", other) + } + + otherVal, err := jsonNum.Int64() + + if err != nil { + return fmt.Errorf("expected json.Number to be parseable as int64 value for Int64Exact check: %s", err) + } + + if otherVal != v.value { + return fmt.Errorf("expected value %d for Int64Exact check, got: %d", v.value, otherVal) + } + + return nil +} + +// String returns the string representation of the int64 value. +func (v int64Exact) String() string { + return strconv.FormatInt(v.value, 10) +} + +// Int64Exact returns a Check for asserting equality between the +// supplied int64 and the value passed to the CheckValue method. +func Int64Exact(value int64) int64Exact { + return int64Exact{ + value: value, + } +} diff --git a/knownvalue/int64_test.go b/knownvalue/int64_test.go new file mode 100644 index 000000000..f367f33a0 --- /dev/null +++ b/knownvalue/int64_test.go @@ -0,0 +1,75 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestInt64Value_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.Int64Exact(0), + expectedError: fmt.Errorf("expected json.Number value for Int64Exact check, got: "), + }, + "zero-other": { + self: knownvalue.Int64Exact(0), + other: json.Number("0"), // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.Int64Exact(1234), + expectedError: fmt.Errorf("expected json.Number value for Int64Exact check, got: "), + }, + "wrong-type": { + self: knownvalue.Int64Exact(1234), + other: json.Number("str"), + expectedError: fmt.Errorf("expected json.Number to be parseable as int64 value for Int64Exact check: strconv.ParseInt: parsing \"str\": invalid syntax"), + }, + "not-equal": { + self: knownvalue.Int64Exact(1234), + other: json.Number("4321"), + expectedError: fmt.Errorf("expected value 1234 for Int64Exact check, got: 4321"), + }, + "equal": { + self: knownvalue.Int64Exact(1234), + other: json.Number("1234"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64Value_String(t *testing.T) { + t.Parallel() + + got := knownvalue.Int64Exact(1234567890123).String() + + if diff := cmp.Diff(got, "1234567890123"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/list.go b/knownvalue/list.go new file mode 100644 index 000000000..500175241 --- /dev/null +++ b/knownvalue/list.go @@ -0,0 +1,67 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" +) + +var _ Check = listExact{} + +type listExact struct { + value []Check +} + +// CheckValue determines whether the passed value is of type []any, and +// contains matching slice entries in the same sequence. +func (v listExact) CheckValue(other any) error { + otherVal, ok := other.([]any) + + if !ok { + return fmt.Errorf("expected []any value for ListExact check, got: %T", other) + } + + if len(otherVal) != len(v.value) { + expectedElements := "elements" + actualElements := "elements" + + if len(v.value) == 1 { + expectedElements = "element" + } + + if len(otherVal) == 1 { + actualElements = "element" + } + + return fmt.Errorf("expected %d %s for ListExact check, got %d %s", len(v.value), expectedElements, len(otherVal), actualElements) + } + + for i := 0; i < len(v.value); i++ { + if err := v.value[i].CheckValue(otherVal[i]); err != nil { + return fmt.Errorf("list element index %d: %s", i, err) + } + } + + return nil +} + +// String returns the string representation of the value. +func (v listExact) String() string { + var listVals []string + + for _, val := range v.value { + listVals = append(listVals, val.String()) + } + + return fmt.Sprintf("%s", listVals) +} + +// ListExact returns a Check for asserting equality between the +// supplied []Check and the value passed to the CheckValue method. +// This is an order-dependent check. +func ListExact(value []Check) listExact { + return listExact{ + value: value, + } +} diff --git a/knownvalue/list_partial.go b/knownvalue/list_partial.go new file mode 100644 index 000000000..7d6e7ee20 --- /dev/null +++ b/knownvalue/list_partial.go @@ -0,0 +1,89 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "bytes" + "fmt" + "sort" + "strings" +) + +var _ Check = listPartial{} + +type listPartial struct { + value map[int]Check +} + +// CheckValue determines whether the passed value is of type []any, and +// contains matching slice entries in the same sequence. +func (v listPartial) CheckValue(other any) error { + otherVal, ok := other.([]any) + + if !ok { + return fmt.Errorf("expected []any value for ListPartial check, got: %T", other) + } + + var keys []int + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + if len(otherVal) <= k { + return fmt.Errorf("missing element index %d for ListPartial check", k) + } + + if err := v.value[k].CheckValue(otherVal[k]); err != nil { + return fmt.Errorf("list element %d: %s", k, err) + } + } + + return nil +} + +// String returns the string representation of the value. +func (v listPartial) String() string { + var b bytes.Buffer + + b.WriteString("[") + + var keys []int + + var listVals []string + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + listVals = append(listVals, fmt.Sprintf("%d:%s", k, v.value[k])) + } + + b.WriteString(strings.Join(listVals, " ")) + + b.WriteString("]") + + return b.String() +} + +// ListPartial returns a Check for asserting partial equality between the +// supplied map[int]Check and the value passed to the CheckValue method. The +// map keys represent the zero-ordered element indices within the list that is +// being checked. Only the elements at the indices defined within the +// supplied map[int]Check are checked. +func ListPartial(value map[int]Check) listPartial { + return listPartial{ + value: value, + } +} diff --git a/knownvalue/list_partial_test.go b/knownvalue/list_partial_test.go new file mode 100644 index 000000000..dd95b49e3 --- /dev/null +++ b/knownvalue/list_partial_test.go @@ -0,0 +1,140 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestListValuePartial_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.ListPartial(map[int]knownvalue.Check{}), + expectedError: fmt.Errorf("expected []any value for ListPartial check, got: "), + }, + "zero-other": { + self: knownvalue.ListPartial(map[int]knownvalue.Check{}), + other: []any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.Float64Exact(4.56), + 3: knownvalue.Float64Exact(7.89), + }), + expectedError: fmt.Errorf("expected []any value for ListPartial check, got: "), + }, + "wrong-type": { + self: knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.Float64Exact(4.56), + 3: knownvalue.Float64Exact(7.89), + }), + other: 1.234, + expectedError: fmt.Errorf("expected []any value for ListPartial check, got: float64"), + }, + "empty": { + self: knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.Float64Exact(4.56), + 3: knownvalue.Float64Exact(7.89), + }), + other: []any{}, + expectedError: fmt.Errorf("missing element index 0 for ListPartial check"), + }, + "wrong-length": { + self: knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.Float64Exact(4.56), + 3: knownvalue.Float64Exact(7.89), + }), + other: []any{ + json.Number("1.23"), + json.Number("4.56"), + }, + expectedError: fmt.Errorf("missing element index 2 for ListPartial check"), + }, + "not-equal": { + self: knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.Float64Exact(4.56), + 3: knownvalue.Float64Exact(7.89), + }), + other: []any{ + json.Number("1.23"), + json.Number("4.56"), + json.Number("6.54"), + json.Number("5.46"), + }, + expectedError: fmt.Errorf("list element 2: expected value 4.56 for Float64Exact check, got: 6.54"), + }, + "wrong-order": { + self: knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.Float64Exact(4.56), + 3: knownvalue.Float64Exact(7.89), + }), + other: []any{ + json.Number("1.23"), + json.Number("0.00"), + json.Number("7.89"), + json.Number("4.56"), + }, + expectedError: fmt.Errorf("list element 2: expected value 4.56 for Float64Exact check, got: 7.89"), + }, + "equal": { + self: knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.Float64Exact(4.56), + 3: knownvalue.Float64Exact(7.89), + }), + other: []any{ + json.Number("1.23"), + json.Number("0.00"), + json.Number("4.56"), + json.Number("7.89"), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListValuePartialPartial_String(t *testing.T) { + t.Parallel() + + got := knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.Float64Exact(1.23), + 2: knownvalue.Float64Exact(4.56), + 3: knownvalue.Float64Exact(7.89), + }).String() + + if diff := cmp.Diff(got, "[0:1.23 2:4.56 3:7.89]"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/list_size.go b/knownvalue/list_size.go new file mode 100644 index 000000000..d95192310 --- /dev/null +++ b/knownvalue/list_size.go @@ -0,0 +1,55 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" + "strconv" +) + +var _ Check = listSizeExact{} + +type listSizeExact struct { + size int +} + +// CheckValue verifies that the passed value is a list, map, object, +// or set, and contains a matching number of elements. +func (v listSizeExact) CheckValue(other any) error { + otherVal, ok := other.([]any) + + if !ok { + return fmt.Errorf("expected []any value for ListSizeExact check, got: %T", other) + } + + if len(otherVal) != v.size { + expectedElements := "elements" + actualElements := "elements" + + if v.size == 1 { + expectedElements = "element" + } + + if len(otherVal) == 1 { + actualElements = "element" + } + + return fmt.Errorf("expected %d %s for ListSizeExact check, got %d %s", v.size, expectedElements, len(otherVal), actualElements) + } + + return nil +} + +// String returns the string representation of the value. +func (v listSizeExact) String() string { + return strconv.FormatInt(int64(v.size), 10) +} + +// ListSizeExact returns a Check for asserting that +// a list has size elements. +func ListSizeExact(size int) listSizeExact { + return listSizeExact{ + size: size, + } +} diff --git a/knownvalue/list_size_test.go b/knownvalue/list_size_test.go new file mode 100644 index 000000000..1ca2fd547 --- /dev/null +++ b/knownvalue/list_size_test.go @@ -0,0 +1,86 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestListElements_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.ListSizeExact(0), + expectedError: fmt.Errorf("expected []any value for ListSizeExact check, got: "), + }, + "zero-other": { + self: knownvalue.ListSizeExact(0), + other: []any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.ListSizeExact(3), + expectedError: fmt.Errorf("expected []any value for ListSizeExact check, got: "), + }, + "wrong-type": { + self: knownvalue.ListSizeExact(3), + other: 1.234, + expectedError: fmt.Errorf("expected []any value for ListSizeExact check, got: float64"), + }, + "empty": { + self: knownvalue.ListSizeExact(3), + other: []any{}, + expectedError: fmt.Errorf("expected 3 elements for ListSizeExact check, got 0 elements"), + }, + "wrong-length": { + self: knownvalue.ListSizeExact(3), + other: []any{ + int64(123), + int64(456), + }, + expectedError: fmt.Errorf("expected 3 elements for ListSizeExact check, got 2 elements"), + }, + "equal": { + self: knownvalue.ListSizeExact(3), + other: []any{ + int64(123), + int64(456), + int64(789), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListElements_String(t *testing.T) { + t.Parallel() + + got := knownvalue.ListSizeExact(2).String() + + if diff := cmp.Diff(got, "2"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/list_test.go b/knownvalue/list_test.go new file mode 100644 index 000000000..8b11f1c96 --- /dev/null +++ b/knownvalue/list_test.go @@ -0,0 +1,137 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestListValue_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.ListExact([]knownvalue.Check{}), + expectedError: fmt.Errorf("expected []any value for ListExact check, got: "), + }, + "zero-other": { + self: knownvalue.ListExact([]knownvalue.Check{}), + other: []any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + expectedError: fmt.Errorf("expected []any value for ListExact check, got: "), + }, + "wrong-type": { + self: knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: 1.234, + expectedError: fmt.Errorf("expected []any value for ListExact check, got: float64"), + }, + "empty": { + self: knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{}, + expectedError: fmt.Errorf("expected 3 elements for ListExact check, got 0 elements"), + }, + "wrong-length": { + self: knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{ + int64(123), + int64(456), + }, + expectedError: fmt.Errorf("expected 3 elements for ListExact check, got 2 elements"), + }, + "not-equal": { + self: knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{ + json.Number("123"), + json.Number("456"), + json.Number("654"), + }, + expectedError: fmt.Errorf("list element index 2: expected value 789 for Int64Exact check, got: 654"), + }, + "wrong-order": { + self: knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{ + json.Number("123"), + json.Number("789"), + json.Number("456"), + }, + expectedError: fmt.Errorf("list element index 1: expected value 456 for Int64Exact check, got: 789"), + }, + "equal": { + self: knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{ + json.Number("123"), + json.Number("456"), + json.Number("789"), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListValue_String(t *testing.T) { + t.Parallel() + + got := knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }).String() + + if diff := cmp.Diff(got, "[123 456 789]"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/map.go b/knownvalue/map.go new file mode 100644 index 000000000..f4027c9df --- /dev/null +++ b/knownvalue/map.go @@ -0,0 +1,93 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" + "sort" +) + +var _ Check = mapExact{} + +type mapExact struct { + value map[string]Check +} + +// CheckValue determines whether the passed value is of type map[string]any, and +// contains matching map entries. +func (v mapExact) CheckValue(other any) error { + otherVal, ok := other.(map[string]any) + + if !ok { + return fmt.Errorf("expected map[string]any value for MapExact check, got: %T", other) + } + + if len(otherVal) != len(v.value) { + expectedElements := "elements" + actualElements := "elements" + + if len(v.value) == 1 { + expectedElements = "element" + } + + if len(otherVal) == 1 { + actualElements = "element" + } + + return fmt.Errorf("expected %d %s for MapExact check, got %d %s", len(v.value), expectedElements, len(otherVal), actualElements) + } + + var keys []string + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + otherValItem, ok := otherVal[k] + + if !ok { + return fmt.Errorf("missing element %s for MapExact check", k) + } + + if err := v.value[k].CheckValue(otherValItem); err != nil { + return fmt.Errorf("%s map element: %s", k, err) + } + } + + return nil +} + +// String returns the string representation of the value. +func (v mapExact) String() string { + var keys []string + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + mapVals := make(map[string]string, len(keys)) + + for _, k := range keys { + mapVals[k] = v.value[k].String() + } + + return fmt.Sprintf("%v", mapVals) +} + +// MapExact returns a Check for asserting equality between the +// supplied map[string]Check and the value passed to the CheckValue method. +func MapExact(value map[string]Check) mapExact { + return mapExact{ + value: value, + } +} diff --git a/knownvalue/map_partial.go b/knownvalue/map_partial.go new file mode 100644 index 000000000..860b2adff --- /dev/null +++ b/knownvalue/map_partial.go @@ -0,0 +1,80 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" + "sort" +) + +var _ Check = mapPartial{} + +type mapPartial struct { + value map[string]Check +} + +// CheckValue determines whether the passed value is of type map[string]any, and +// contains matching map entries. +func (v mapPartial) CheckValue(other any) error { + otherVal, ok := other.(map[string]any) + + if !ok { + return fmt.Errorf("expected map[string]any value for MapPartial check, got: %T", other) + } + + var keys []string + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + otherValItem, ok := otherVal[k] + + if !ok { + return fmt.Errorf("missing element %s for MapPartial check", k) + } + + if err := v.value[k].CheckValue(otherValItem); err != nil { + return fmt.Errorf("%s map element: %s", k, err) + } + } + + return nil +} + +// String returns the string representation of the value. +func (v mapPartial) String() string { + var keys []string + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + mapVals := make(map[string]string, len(keys)) + + for _, k := range keys { + mapVals[k] = v.value[k].String() + } + + return fmt.Sprintf("%v", mapVals) +} + +// MapPartial returns a Check for asserting partial equality between the +// supplied map[string]Check and the value passed to the CheckValue method. Only +// the elements at the map keys defined within the supplied map[string]Check are +// checked. +func MapPartial(value map[string]Check) mapPartial { + return mapPartial{ + value: value, + } +} diff --git a/knownvalue/map_partial_test.go b/knownvalue/map_partial_test.go new file mode 100644 index 000000000..30bceae11 --- /dev/null +++ b/knownvalue/map_partial_test.go @@ -0,0 +1,142 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestMapValuePartial_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{}), + expectedError: fmt.Errorf("expected map[string]any value for MapPartial check, got: "), + }, + "zero-other": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{}), + other: map[string]any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + expectedError: fmt.Errorf("expected map[string]any value for MapPartial check, got: "), + }, + "wrong-type": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: 1.234, + expectedError: fmt.Errorf("expected map[string]any value for MapPartial check, got: float64"), + }, + "empty": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{}, + expectedError: fmt.Errorf("missing element one for MapPartial check"), + }, + "wrong-length": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + }, + expectedError: fmt.Errorf("missing element three for MapPartial check"), + }, + "not-equal": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + "three": json.Number("6.54"), + }, + expectedError: fmt.Errorf("three map element: expected value 7.89 for Float64Exact check, got: 6.54"), + }, + "wrong-order": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("7.89"), + "three": json.Number("4.56"), + }, + expectedError: fmt.Errorf("three map element: expected value 7.89 for Float64Exact check, got: 4.56"), + }, + "key-not-found": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "four": json.Number("1.23"), + "five": json.Number("7.89"), + "six": json.Number("4.56"), + }, + expectedError: fmt.Errorf("missing element one for MapPartial check"), + }, + "equal": { + self: knownvalue.MapPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + "three": json.Number("7.89"), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapValuePartial_String(t *testing.T) { + t.Parallel() + + got := knownvalue.MapPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }).String() + + if diff := cmp.Diff(got, "map[one:1.23 three:7.89]"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/map_size.go b/knownvalue/map_size.go new file mode 100644 index 000000000..c0966823c --- /dev/null +++ b/knownvalue/map_size.go @@ -0,0 +1,55 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" + "strconv" +) + +var _ Check = mapSizeExact{} + +type mapSizeExact struct { + size int +} + +// CheckValue verifies that the passed value is a list, map, object, +// or set, and contains a matching number of elements. +func (v mapSizeExact) CheckValue(other any) error { + otherVal, ok := other.(map[string]any) + + if !ok { + return fmt.Errorf("expected map[string]any value for MapSizeExact check, got: %T", other) + } + + if len(otherVal) != v.size { + expectedElements := "elements" + actualElements := "elements" + + if v.size == 1 { + expectedElements = "element" + } + + if len(otherVal) == 1 { + actualElements = "element" + } + + return fmt.Errorf("expected %d %s for MapSizeExact check, got %d %s", v.size, expectedElements, len(otherVal), actualElements) + } + + return nil +} + +// String returns the string representation of the value. +func (v mapSizeExact) String() string { + return strconv.Itoa(v.size) +} + +// MapSizeExact returns a Check for asserting that +// a map has size elements. +func MapSizeExact(size int) mapSizeExact { + return mapSizeExact{ + size: size, + } +} diff --git a/knownvalue/map_size_test.go b/knownvalue/map_size_test.go new file mode 100644 index 000000000..9cf171738 --- /dev/null +++ b/knownvalue/map_size_test.go @@ -0,0 +1,86 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestMapElements_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.MapSizeExact(0), + expectedError: fmt.Errorf("expected map[string]any value for MapSizeExact check, got: "), + }, + "zero-other": { + self: knownvalue.MapSizeExact(0), + other: map[string]any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.MapSizeExact(3), + expectedError: fmt.Errorf("expected map[string]any value for MapSizeExact check, got: "), + }, + "wrong-type": { + self: knownvalue.MapSizeExact(3), + other: 1.234, + expectedError: fmt.Errorf("expected map[string]any value for MapSizeExact check, got: float64"), + }, + "empty": { + self: knownvalue.MapSizeExact(3), + other: map[string]any{}, + expectedError: fmt.Errorf("expected 3 elements for MapSizeExact check, got 0 elements"), + }, + "wrong-length": { + self: knownvalue.MapSizeExact(3), + other: map[string]any{ + "one": int64(123), + "two": int64(456), + }, + expectedError: fmt.Errorf("expected 3 elements for MapSizeExact check, got 2 elements"), + }, + "equal": { + self: knownvalue.MapSizeExact(3), + other: map[string]any{ + "one": int64(123), + "two": int64(456), + "three": int64(789), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapElements_String(t *testing.T) { + t.Parallel() + + got := knownvalue.MapSizeExact(2).String() + + if diff := cmp.Diff(got, "2"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/map_test.go b/knownvalue/map_test.go new file mode 100644 index 000000000..09e6736b5 --- /dev/null +++ b/knownvalue/map_test.go @@ -0,0 +1,150 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestMapValue_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.MapExact(map[string]knownvalue.Check{}), + expectedError: fmt.Errorf("expected map[string]any value for MapExact check, got: "), + }, + "zero-other": { + self: knownvalue.MapExact(map[string]knownvalue.Check{}), + other: map[string]any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.MapExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + expectedError: fmt.Errorf("expected map[string]any value for MapExact check, got: "), + }, + "wrong-type": { + self: knownvalue.MapExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: 1.234, + expectedError: fmt.Errorf("expected map[string]any value for MapExact check, got: float64"), + }, + "empty": { + self: knownvalue.MapExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{}, + expectedError: fmt.Errorf("expected 3 elements for MapExact check, got 0 elements"), + }, + "wrong-length": { + self: knownvalue.MapExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + }, + expectedError: fmt.Errorf("expected 3 elements for MapExact check, got 2 elements"), + }, + "not-equal": { + self: knownvalue.MapExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + "three": json.Number("6.54"), + }, + expectedError: fmt.Errorf("three map element: expected value 7.89 for Float64Exact check, got: 6.54"), + }, + "wrong-order": { + self: knownvalue.MapExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("7.89"), + "three": json.Number("4.56"), + }, + expectedError: fmt.Errorf("three map element: expected value 7.89 for Float64Exact check, got: 4.56"), + }, + "key-not-found": { + self: knownvalue.MapExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "four": json.Number("1.23"), + "five": json.Number("7.89"), + "six": json.Number("4.56"), + }, + expectedError: fmt.Errorf("missing element one for MapExact check"), + }, + "equal": { + self: knownvalue.MapExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + "three": json.Number("7.89"), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapValue_String(t *testing.T) { + t.Parallel() + + got := knownvalue.MapExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }).String() + + if diff := cmp.Diff(got, "map[one:1.23 three:7.89 two:4.56]"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/number.go b/knownvalue/number.go new file mode 100644 index 000000000..d101b1f68 --- /dev/null +++ b/knownvalue/number.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "encoding/json" + "fmt" + "math/big" +) + +var _ Check = numberExact{} + +type numberExact struct { + value *big.Float +} + +// CheckValue determines whether the passed value is of type *big.Float, and +// contains a matching *big.Float value. +func (v numberExact) CheckValue(other any) error { + if v.value == nil { + return fmt.Errorf("value in NumberExact check is nil") + } + + jsonNum, ok := other.(json.Number) + + if !ok { + return fmt.Errorf("expected json.Number value for NumberExact check, got: %T", other) + } + + otherVal, _, err := big.ParseFloat(jsonNum.String(), 10, 512, big.ToNearestEven) + + if err != nil { + return fmt.Errorf("expected json.Number to be parseable as big.Float value for NumberExact check: %s", err) + } + + if v.value.Cmp(otherVal) != 0 { + return fmt.Errorf("expected value %s for NumberExact check, got: %s", v.String(), otherVal.Text('f', -1)) + } + + return nil +} + +// String returns the string representation of the *big.Float value. +func (v numberExact) String() string { + return v.value.Text('f', -1) +} + +// NumberExact returns a Check for asserting equality between the +// supplied *big.Float and the value passed to the CheckValue method. +// The CheckValue method uses 512-bit precision to perform this assertion. +func NumberExact(value *big.Float) numberExact { + return numberExact{ + value: value, + } +} diff --git a/knownvalue/number_test.go b/knownvalue/number_test.go new file mode 100644 index 000000000..50f6e12d5 --- /dev/null +++ b/knownvalue/number_test.go @@ -0,0 +1,89 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "math/big" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestNumberValue_Equal(t *testing.T) { + t.Parallel() + + bigFloat, _, err := big.ParseFloat("1.797693134862315797693134862315797693134862315", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.NumberExact(nil), + expectedError: fmt.Errorf("value in NumberExact check is nil"), + }, + "zero-other": { + self: knownvalue.NumberExact(nil), + other: json.Number("1.797693134862315797693134862315797693134862314"), // checking against the underlying value field zero-value + expectedError: fmt.Errorf("value in NumberExact check is nil"), + }, + "nil": { + self: knownvalue.NumberExact(bigFloat), + expectedError: fmt.Errorf("expected json.Number value for NumberExact check, got: "), + }, + "wrong-type": { + self: knownvalue.NumberExact(bigFloat), + other: json.Number("str"), + expectedError: fmt.Errorf("expected json.Number to be parseable as big.Float value for NumberExact check: number has no digits"), + }, + "not-equal": { + self: knownvalue.NumberExact(bigFloat), + other: json.Number("1.797693134862315797693134862315797693134862314"), + expectedError: fmt.Errorf("expected value 1.797693134862315797693134862315797693134862315 for NumberExact check, got: 1.797693134862315797693134862315797693134862314"), + }, + "equal": { + self: knownvalue.NumberExact(bigFloat), + other: json.Number("1.797693134862315797693134862315797693134862315"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberValue_String(t *testing.T) { + t.Parallel() + + bigFloat, _, err := big.ParseFloat("1.797693134862315797693134862315797693134862315", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + got := knownvalue.NumberExact(bigFloat).String() + + if diff := cmp.Diff(got, "1.797693134862315797693134862315797693134862315"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/object.go b/knownvalue/object.go new file mode 100644 index 000000000..87eabfb63 --- /dev/null +++ b/knownvalue/object.go @@ -0,0 +1,94 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" + "sort" +) + +var _ Check = objectExact{} + +type objectExact struct { + value map[string]Check +} + +// CheckValue determines whether the passed value is of type map[string]any, and +// contains matching object entries. +func (v objectExact) CheckValue(other any) error { + otherVal, ok := other.(map[string]any) + + if !ok { + return fmt.Errorf("expected map[string]any value for ObjectExact check, got: %T", other) + } + + if len(otherVal) != len(v.value) { + expectedAttributes := "attributes" + actualAttributes := "attributes" + + if len(v.value) == 1 { + expectedAttributes = "attribute" + } + + if len(otherVal) == 1 { + actualAttributes = "attribute" + } + + return fmt.Errorf("expected %d %s for ObjectExact check, got %d %s", len(v.value), expectedAttributes, len(otherVal), actualAttributes) + } + + var keys []string + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + otherValItem, ok := otherVal[k] + + if !ok { + return fmt.Errorf("missing attribute %s for ObjectExact check", k) + } + + if err := v.value[k].CheckValue(otherValItem); err != nil { + return fmt.Errorf("%s object attribute: %s", k, err) + } + } + + return nil +} + +// String returns the string representation of the value. +func (v objectExact) String() string { + var keys []string + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + mapVals := make(map[string]string, len(keys)) + + for _, k := range keys { + mapVals[k] = v.value[k].String() + } + + return fmt.Sprintf("%v", mapVals) +} + +// ObjectExact returns a Check for asserting equality between the supplied +// map[string]Check and the value passed to the CheckValue method. The map +// keys represent object attribute names. +func ObjectExact(value map[string]Check) objectExact { + return objectExact{ + value: value, + } +} diff --git a/knownvalue/object_partial.go b/knownvalue/object_partial.go new file mode 100644 index 000000000..775ab4c34 --- /dev/null +++ b/knownvalue/object_partial.go @@ -0,0 +1,80 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" + "sort" +) + +var _ Check = objectPartial{} + +type objectPartial struct { + value map[string]Check +} + +// CheckValue determines whether the passed value is of type map[string]any, and +// contains matching map entries. +func (v objectPartial) CheckValue(other any) error { + otherVal, ok := other.(map[string]any) + + if !ok { + return fmt.Errorf("expected map[string]any value for ObjectPartial check, got: %T", other) + } + + var keys []string + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + otherValItem, ok := otherVal[k] + + if !ok { + return fmt.Errorf("missing attribute %s for ObjectPartial check", k) + } + + if err := v.value[k].CheckValue(otherValItem); err != nil { + return fmt.Errorf("%s object attribute: %s", k, err) + } + } + + return nil +} + +// String returns the string representation of the value. +func (v objectPartial) String() string { + var keys []string + + for k := range v.value { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + mapVals := make(map[string]string, len(keys)) + + for _, k := range keys { + mapVals[k] = v.value[k].String() + } + + return fmt.Sprintf("%v", mapVals) +} + +// ObjectPartial returns a Check for asserting partial equality between the +// supplied map[string]Check and the value passed to the CheckValue method. The map +// keys represent object attribute names. Only the object attributes defined by the +// map keys within the supplied map[string]Check are checked. +func ObjectPartial(value map[string]Check) objectPartial { + return objectPartial{ + value: value, + } +} diff --git a/knownvalue/object_partial_test.go b/knownvalue/object_partial_test.go new file mode 100644 index 000000000..20884d0e4 --- /dev/null +++ b/knownvalue/object_partial_test.go @@ -0,0 +1,142 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestObjectValuePartial_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{}), + expectedError: fmt.Errorf("expected map[string]any value for ObjectPartial check, got: "), + }, + "zero-other": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{}), + other: map[string]any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + expectedError: fmt.Errorf("expected map[string]any value for ObjectPartial check, got: "), + }, + "wrong-type": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: 1.234, + expectedError: fmt.Errorf("expected map[string]any value for ObjectPartial check, got: float64"), + }, + "empty": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{}, + expectedError: fmt.Errorf("missing attribute one for ObjectPartial check"), + }, + "wrong-length": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + }, + expectedError: fmt.Errorf("missing attribute three for ObjectPartial check"), + }, + "not-equal": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + "three": json.Number("6.54"), + }, + expectedError: fmt.Errorf("three object attribute: expected value 7.89 for Float64Exact check, got: 6.54"), + }, + "wrong-order": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("7.89"), + "three": json.Number("4.56"), + }, + expectedError: fmt.Errorf("three object attribute: expected value 7.89 for Float64Exact check, got: 4.56"), + }, + "key-not-found": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "four": json.Number("1.23"), + "five": json.Number("7.89"), + "six": json.Number("4.56"), + }, + expectedError: fmt.Errorf("missing attribute one for ObjectPartial check"), + }, + "equal": { + self: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + "three": json.Number("7.89"), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectValuePartial_String(t *testing.T) { + t.Parallel() + + got := knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "three": knownvalue.Float64Exact(7.89), + }).String() + + if diff := cmp.Diff(got, "map[one:1.23 three:7.89]"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/object_test.go b/knownvalue/object_test.go new file mode 100644 index 000000000..07693fbff --- /dev/null +++ b/knownvalue/object_test.go @@ -0,0 +1,150 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestObjectValue_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{}), + expectedError: fmt.Errorf("expected map[string]any value for ObjectExact check, got: "), + }, + "zero-other": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{}), + other: map[string]any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + expectedError: fmt.Errorf("expected map[string]any value for ObjectExact check, got: "), + }, + "wrong-type": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: 1.234, + expectedError: fmt.Errorf("expected map[string]any value for ObjectExact check, got: float64"), + }, + "empty": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{}, + expectedError: fmt.Errorf("expected 3 attributes for ObjectExact check, got 0 attributes"), + }, + "wrong-length": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + }, + expectedError: fmt.Errorf("expected 3 attributes for ObjectExact check, got 2 attributes"), + }, + "not-equal": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + "three": json.Number("6.54"), + }, + expectedError: fmt.Errorf("three object attribute: expected value 7.89 for Float64Exact check, got: 6.54"), + }, + "wrong-order": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("7.89"), + "three": json.Number("4.56"), + }, + expectedError: fmt.Errorf("three object attribute: expected value 7.89 for Float64Exact check, got: 4.56"), + }, + "key-not-found": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "four": json.Number("1.23"), + "five": json.Number("7.89"), + "six": json.Number("4.56"), + }, + expectedError: fmt.Errorf("missing attribute one for ObjectExact check"), + }, + "equal": { + self: knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }), + other: map[string]any{ + "one": json.Number("1.23"), + "two": json.Number("4.56"), + "three": json.Number("7.89"), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectValue_String(t *testing.T) { + t.Parallel() + + got := knownvalue.ObjectExact(map[string]knownvalue.Check{ + "one": knownvalue.Float64Exact(1.23), + "two": knownvalue.Float64Exact(4.56), + "three": knownvalue.Float64Exact(7.89), + }).String() + + if diff := cmp.Diff(got, "map[one:1.23 three:7.89 two:4.56]"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/set.go b/knownvalue/set.go new file mode 100644 index 000000000..206f26698 --- /dev/null +++ b/knownvalue/set.go @@ -0,0 +1,86 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" +) + +var _ Check = setExact{} + +type setExact struct { + value []Check +} + +// CheckValue determines whether the passed value is of type []any, and +// contains matching slice entries independent of the sequence. +func (v setExact) CheckValue(other any) error { + otherVal, ok := other.([]any) + + if !ok { + return fmt.Errorf("expected []any value for SetExact check, got: %T", other) + } + + if len(otherVal) != len(v.value) { + expectedElements := "elements" + actualElements := "elements" + + if len(v.value) == 1 { + expectedElements = "element" + } + + if len(otherVal) == 1 { + actualElements = "element" + } + + return fmt.Errorf("expected %d %s for SetExact check, got %d %s", len(v.value), expectedElements, len(otherVal), actualElements) + } + + otherValCopy := make([]any, len(otherVal)) + + copy(otherValCopy, otherVal) + + for i := 0; i < len(v.value); i++ { + err := fmt.Errorf("missing value %s for SetExact check", v.value[i].String()) + + for j := 0; j < len(otherValCopy); j++ { + checkValueErr := v.value[i].CheckValue(otherValCopy[j]) + + if checkValueErr == nil { + otherValCopy[j] = otherValCopy[len(otherValCopy)-1] + otherValCopy = otherValCopy[:len(otherValCopy)-1] + + err = nil + + break + } + } + + if err != nil { + return err + } + } + + return nil +} + +// String returns the string representation of the value. +func (v setExact) String() string { + var setVals []string + + for _, val := range v.value { + setVals = append(setVals, val.String()) + } + + return fmt.Sprintf("%s", setVals) +} + +// SetExact returns a Check for asserting equality between the +// supplied []Check and the value passed to the CheckValue method. +// This is an order-independent check. +func SetExact(value []Check) setExact { + return setExact{ + value: value, + } +} diff --git a/knownvalue/set_partial.go b/knownvalue/set_partial.go new file mode 100644 index 000000000..dcbcd2ff1 --- /dev/null +++ b/knownvalue/set_partial.go @@ -0,0 +1,72 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" +) + +var _ Check = setPartial{} + +type setPartial struct { + value []Check +} + +// CheckValue determines whether the passed value is of type []any, and +// contains matching slice entries in any sequence. +func (v setPartial) CheckValue(other any) error { + otherVal, ok := other.([]any) + + if !ok { + return fmt.Errorf("expected []any value for SetPartial check, got: %T", other) + } + + otherValCopy := make([]any, len(otherVal)) + + copy(otherValCopy, otherVal) + + for i := 0; i < len(v.value); i++ { + err := fmt.Errorf("missing value %s for SetPartial check", v.value[i].String()) + + for j := 0; j < len(otherValCopy); j++ { + checkValueErr := v.value[i].CheckValue(otherValCopy[j]) + + if checkValueErr == nil { + otherValCopy[j] = otherValCopy[len(otherValCopy)-1] + otherValCopy = otherValCopy[:len(otherValCopy)-1] + + err = nil + + break + } + } + + if err != nil { + return err + } + } + + return nil +} + +// String returns the string representation of the value. +func (v setPartial) String() string { + var setVals []string + + for _, val := range v.value { + setVals = append(setVals, val.String()) + } + + return fmt.Sprintf("%s", setVals) +} + +// SetPartial returns a Check for asserting partial equality between the +// supplied []Check and the value passed to the CheckValue method. Only the +// elements defined within the supplied []Check are checked. This is an +// order-independent check. +func SetPartial(value []Check) setPartial { + return setPartial{ + value: value, + } +} diff --git a/knownvalue/set_partial_test.go b/knownvalue/set_partial_test.go new file mode 100644 index 000000000..b58baff50 --- /dev/null +++ b/knownvalue/set_partial_test.go @@ -0,0 +1,127 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestSetValuePartial_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.SetPartial([]knownvalue.Check{}), + expectedError: fmt.Errorf("expected []any value for SetPartial check, got: "), + }, + "zero-other": { + self: knownvalue.SetPartial([]knownvalue.Check{}), + other: []any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.Float64Exact(1.23), + knownvalue.Float64Exact(4.56), + knownvalue.Float64Exact(7.89), + }), + expectedError: fmt.Errorf("expected []any value for SetPartial check, got: "), + }, + "wrong-type": { + self: knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.Float64Exact(1.23), + knownvalue.Float64Exact(4.56), + knownvalue.Float64Exact(7.89), + }), + other: 1.234, + expectedError: fmt.Errorf("expected []any value for SetPartial check, got: float64"), + }, + "equal-empty": { + self: knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.Float64Exact(1.23), + knownvalue.Float64Exact(4.56), + knownvalue.Float64Exact(7.89), + }), + other: []any{}, + expectedError: fmt.Errorf("missing value 1.23 for SetPartial check"), + }, + "not-equal": { + self: knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.Float64Exact(1.23), + knownvalue.Float64Exact(4.56), + knownvalue.Float64Exact(7.89), + }), + other: []any{ + json.Number("1.23"), + json.Number("4.56"), + json.Number("6.54"), + json.Number("5.46"), + }, + expectedError: fmt.Errorf("missing value 7.89 for SetPartial check"), + }, + "equal-different-order": { + self: knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.Float64Exact(1.23), + knownvalue.Float64Exact(4.56), + knownvalue.Float64Exact(7.89), + }), + other: []any{ + json.Number("1.23"), + json.Number("0.00"), + json.Number("7.89"), + json.Number("4.56"), + }, + }, + "equal-same-order": { + self: knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.Float64Exact(1.23), + knownvalue.Float64Exact(4.56), + knownvalue.Float64Exact(7.89), + }), + other: []any{ + json.Number("1.23"), + json.Number("0.00"), + json.Number("4.56"), + json.Number("7.89"), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetValuePartial_String(t *testing.T) { + t.Parallel() + + got := knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.Float64Exact(1.23), + knownvalue.Float64Exact(4.56), + knownvalue.Float64Exact(7.89), + }).String() + + if diff := cmp.Diff(got, "[1.23 4.56 7.89]"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/set_size.go b/knownvalue/set_size.go new file mode 100644 index 000000000..aa3cce170 --- /dev/null +++ b/knownvalue/set_size.go @@ -0,0 +1,55 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import ( + "fmt" + "strconv" +) + +var _ Check = setSizeExact{} + +type setSizeExact struct { + size int +} + +// CheckValue verifies that the passed value is a list, map, object, +// or set, and contains a matching number of elements. +func (v setSizeExact) CheckValue(other any) error { + otherVal, ok := other.([]any) + + if !ok { + return fmt.Errorf("expected []any value for SetElementExact check, got: %T", other) + } + + if len(otherVal) != v.size { + expectedElements := "elements" + actualElements := "elements" + + if v.size == 1 { + expectedElements = "element" + } + + if len(otherVal) == 1 { + actualElements = "element" + } + + return fmt.Errorf("expected %d %s for SetElementExact check, got %d %s", v.size, expectedElements, len(otherVal), actualElements) + } + + return nil +} + +// String returns the string representation of the value. +func (v setSizeExact) String() string { + return strconv.FormatInt(int64(v.size), 10) +} + +// SetSizeExact returns a Check for asserting that +// a set has size elements. +func SetSizeExact(size int) setSizeExact { + return setSizeExact{ + size: size, + } +} diff --git a/knownvalue/set_size_test.go b/knownvalue/set_size_test.go new file mode 100644 index 000000000..aed263be6 --- /dev/null +++ b/knownvalue/set_size_test.go @@ -0,0 +1,86 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestSetElements_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.SetSizeExact(0), + expectedError: fmt.Errorf("expected []any value for SetElementExact check, got: "), + }, + "zero-other": { + self: knownvalue.SetSizeExact(0), + other: []any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.SetSizeExact(3), + expectedError: fmt.Errorf("expected []any value for SetElementExact check, got: "), + }, + "wrong-type": { + self: knownvalue.SetSizeExact(3), + other: 1.234, + expectedError: fmt.Errorf("expected []any value for SetElementExact check, got: float64"), + }, + "empty": { + self: knownvalue.SetSizeExact(3), + other: []any{}, + expectedError: fmt.Errorf("expected 3 elements for SetElementExact check, got 0 elements"), + }, + "wrong-length": { + self: knownvalue.SetSizeExact(3), + other: []any{ + int64(123), + int64(456), + }, + expectedError: fmt.Errorf("expected 3 elements for SetElementExact check, got 2 elements"), + }, + "equal": { + self: knownvalue.SetSizeExact(3), + other: []any{ + int64(123), + int64(456), + int64(789), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetElements_String(t *testing.T) { + t.Parallel() + + got := knownvalue.SetSizeExact(2).String() + + if diff := cmp.Diff(got, "2"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/set_test.go b/knownvalue/set_test.go new file mode 100644 index 000000000..dad5cd849 --- /dev/null +++ b/knownvalue/set_test.go @@ -0,0 +1,136 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestSetValue_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.SetExact([]knownvalue.Check{}), + expectedError: fmt.Errorf("expected []any value for SetExact check, got: "), + }, + "zero-other": { + self: knownvalue.SetExact([]knownvalue.Check{}), + other: []any{}, // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.SetExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + expectedError: fmt.Errorf("expected []any value for SetExact check, got: "), + }, + "wrong-type": { + self: knownvalue.SetExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: 1.234, + expectedError: fmt.Errorf("expected []any value for SetExact check, got: float64"), + }, + "empty": { + self: knownvalue.SetExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{}, + expectedError: fmt.Errorf("expected 3 elements for SetExact check, got 0 elements"), + }, + "wrong-length": { + self: knownvalue.SetExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{ + json.Number("123"), + json.Number("456"), + }, + expectedError: fmt.Errorf("expected 3 elements for SetExact check, got 2 elements"), + }, + "not-equal": { + self: knownvalue.SetExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{ + json.Number("123"), + json.Number("456"), + json.Number("654"), + }, + expectedError: fmt.Errorf("missing value 789 for SetExact check"), + }, + "equal-different-order": { + self: knownvalue.SetExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{ + json.Number("123"), + json.Number("789"), + json.Number("456"), + }, + }, + "equal-same-order": { + self: knownvalue.SetExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }), + other: []any{ + json.Number("123"), + json.Number("456"), + json.Number("789"), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetValue_String(t *testing.T) { + t.Parallel() + + got := knownvalue.SetExact([]knownvalue.Check{ + knownvalue.Int64Exact(123), + knownvalue.Int64Exact(456), + knownvalue.Int64Exact(789), + }).String() + + if diff := cmp.Diff(got, "[123 456 789]"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/knownvalue/string.go b/knownvalue/string.go new file mode 100644 index 000000000..63d03a507 --- /dev/null +++ b/knownvalue/string.go @@ -0,0 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue + +import "fmt" + +var _ Check = stringExact{} + +type stringExact struct { + value string +} + +// CheckValue determines whether the passed value is of type string, and +// contains a matching sequence of bytes. +func (v stringExact) CheckValue(other any) error { + otherVal, ok := other.(string) + + if !ok { + return fmt.Errorf("expected string value for StringExact check, got: %T", other) + } + + if otherVal != v.value { + return fmt.Errorf("expected value %s for StringExact check, got: %s", v.value, otherVal) + } + + return nil +} + +// String returns the string representation of the value. +func (v stringExact) String() string { + return v.value +} + +// StringExact returns a Check for asserting equality between the +// supplied string and a value passed to the CheckValue method. +func StringExact(value string) stringExact { + return stringExact{ + value: value, + } +} diff --git a/knownvalue/string_test.go b/knownvalue/string_test.go new file mode 100644 index 000000000..7d7a8da3a --- /dev/null +++ b/knownvalue/string_test.go @@ -0,0 +1,74 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package knownvalue_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +func TestStringValue_CheckValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + self knownvalue.Check + other any + expectedError error + }{ + "zero-nil": { + self: knownvalue.StringExact(""), + expectedError: fmt.Errorf("expected string value for StringExact check, got: "), + }, + "zero-other": { + self: knownvalue.StringExact(""), + other: "", // checking against the underlying value field zero-value + }, + "nil": { + self: knownvalue.StringExact("str"), + expectedError: fmt.Errorf("expected string value for StringExact check, got: "), + }, + "wrong-type": { + self: knownvalue.StringExact("str"), + other: 1.234, + expectedError: fmt.Errorf("expected string value for StringExact check, got: float64"), + }, + "not-equal": { + self: knownvalue.StringExact("str"), + other: "rts", + expectedError: fmt.Errorf("expected value str for StringExact check, got: rts"), + }, + "equal": { + self: knownvalue.StringExact("str"), + other: "str", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.self.CheckValue(testCase.other) + + if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringValue_String(t *testing.T) { + t.Parallel() + + got := knownvalue.StringExact("str").String() + + if diff := cmp.Diff(got, "str"); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/plancheck/expect_known_output_value.go b/plancheck/expect_known_output_value.go new file mode 100644 index 000000000..33a27e2f5 --- /dev/null +++ b/plancheck/expect_known_output_value.go @@ -0,0 +1,84 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck + +import ( + "context" + "fmt" + "reflect" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource Plan Check +var _ PlanCheck = expectKnownOutputValue{} + +type expectKnownOutputValue struct { + outputAddress string + knownValue knownvalue.Check +} + +// CheckPlan implements the plan check logic. +func (e expectKnownOutputValue) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { + var change *tfjson.Change + + for address, oc := range req.Plan.OutputChanges { + if e.outputAddress == address { + change = oc + + break + } + } + + if change == nil { + resp.Error = fmt.Errorf("%s - Output not found in plan OutputChanges", e.outputAddress) + + return + } + + result, err := tfjsonpath.Traverse(change.After, tfjsonpath.Path{}) + + if err != nil { + resp.Error = err + + return + } + + if result == nil { + resp.Error = fmt.Errorf("value is null for output at path: %s", e.outputAddress) + + return + } + + switch reflect.TypeOf(result).Kind() { + case reflect.Bool, + reflect.Map, + reflect.Slice, + reflect.String: + if err := e.knownValue.CheckValue(result); err != nil { + resp.Error = fmt.Errorf("error checking value for output at path: %s, err: %s", e.outputAddress, err) + + return + } + default: + errorStr := fmt.Sprintf("unrecognised output type: %T, known value type is %T", result, e.knownValue) + errorStr += "\n\nThis is an error in plancheck.ExpectKnownOutputValue.\nPlease report this to the maintainers." + + resp.Error = fmt.Errorf(errorStr) + + return + } +} + +// ExpectKnownOutputValue returns a plan check that asserts that the specified value +// has a known type, and value. +func ExpectKnownOutputValue(outputAddress string, knownValue knownvalue.Check) PlanCheck { + return expectKnownOutputValue{ + outputAddress: outputAddress, + knownValue: knownValue, + } +} diff --git a/plancheck/expect_known_output_value_at_path.go b/plancheck/expect_known_output_value_at_path.go new file mode 100644 index 000000000..056b7c2f7 --- /dev/null +++ b/plancheck/expect_known_output_value_at_path.go @@ -0,0 +1,86 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck + +import ( + "context" + "fmt" + "reflect" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource Plan Check +var _ PlanCheck = expectKnownOutputValueAtPath{} + +type expectKnownOutputValueAtPath struct { + outputAddress string + outputPath tfjsonpath.Path + knownValue knownvalue.Check +} + +// CheckPlan implements the plan check logic. +func (e expectKnownOutputValueAtPath) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { + var change *tfjson.Change + + for address, oc := range req.Plan.OutputChanges { + if e.outputAddress == address { + change = oc + + break + } + } + + if change == nil { + resp.Error = fmt.Errorf("%s - Output not found in plan OutputChanges", e.outputAddress) + + return + } + + result, err := tfjsonpath.Traverse(change.After, e.outputPath) + + if err != nil { + resp.Error = err + + return + } + + if result == nil { + resp.Error = fmt.Errorf("value is null for output at path: %s.%s", e.outputAddress, e.outputPath.String()) + + return + } + + switch reflect.TypeOf(result).Kind() { + case reflect.Bool, + reflect.Map, + reflect.Slice, + reflect.String: + if err := e.knownValue.CheckValue(result); err != nil { + resp.Error = fmt.Errorf("error checking value for output at path: %s.%s, err: %s", e.outputAddress, e.outputPath.String(), err) + + return + } + default: + errorStr := fmt.Sprintf("unrecognised output type: %T, known value type is %T", result, e.knownValue) + errorStr += "\n\nThis is an error in plancheck.ExpectKnownOutputValueAtPath.\nPlease report this to the maintainers." + + resp.Error = fmt.Errorf(errorStr) + + return + } +} + +// ExpectKnownOutputValueAtPath returns a plan check that asserts that the specified output at the given path +// has a known type and value. +func ExpectKnownOutputValueAtPath(outputAddress string, outputPath tfjsonpath.Path, knownValue knownvalue.Check) PlanCheck { + return expectKnownOutputValueAtPath{ + outputAddress: outputAddress, + outputPath: outputPath, + knownValue: knownValue, + } +} diff --git a/plancheck/expect_known_output_value_at_path_test.go b/plancheck/expect_known_output_value_at_path_test.go new file mode 100644 index 000000000..564e9c39e --- /dev/null +++ b/plancheck/expect_known_output_value_at_path_test.go @@ -0,0 +1,1833 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck_test + +import ( + "context" + "fmt" + "math/big" + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestExpectKnownOutputValueAtPath_CheckPlan_ResourceNotFound(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_two_output", + tfjsonpath.New("bool_attribute"), + knownvalue.BoolExact(true), + ), + }, + }, + ExpectError: regexp.MustCompile("test_resource_two_output - Output not found in plan OutputChanges"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_AttributeValueNull(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" {} + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.BoolExact(true), + ), + }, + }, + ExpectError: regexp.MustCompile("value is null for output at path: test_resource_one_output.bool_attribute"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Bool(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.BoolExact(true), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Bool_KnownValueWrongType(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.Float64Exact(1.23), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.bool_attribute, err: expected json\.Number value for Float64Exact check, got: bool`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Bool_KnownValueWrongValue(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("bool_attribute"), + knownvalue.BoolExact(false), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: test_resource_one_output.bool_attribute, err: expected value false for BoolExact check, got: true"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Float64(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("float_attribute"), + knownvalue.Float64Exact(1.23), + ), + }, + }, + }, + }, + }) +} + +// We do not need equivalent tests for Int64 and Number as they all test the same logic. +func TestExpectKnownOutputValueAtPath_CheckPlan_Float64_KnownValueWrongType(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("float_attribute"), + knownvalue.StringExact("str"), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.float_attribute, err: expected string value for StringExact check, got: json\.Number`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Float64_KnownValueWrongValue(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("float_attribute"), + knownvalue.Float64Exact(3.21), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: test_resource_one_output.float_attribute, err: expected value 3.21 for Float64Exact check, got: 1.23"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Int64(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.Int64Exact(123), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Int64_KnownValueWrongValue(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.Int64Exact(321), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: test_resource_one_output.int_attribute, err: expected value 321 for Int64Exact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_List(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_List_KnownValueWrongType(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{}), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.list_attribute, err: expected map\[string\]any value for MapExact check, got: \[\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_List_KnownValueWrongValue(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value3"), + knownvalue.StringExact("value4"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.list_attribute, err: list element index 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_ListPartial(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }, + }) +} + +// No need to check KnownValueWrongType for ListPartial as all lists, and sets are []any in +// tfjson.Plan. +func TestExpectKnownOutputValueAtPath_CheckPlan_ListPartial_KnownValueWrongValue(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value3"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.list_attribute, err: list element 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_ListElements(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_ListElements_WrongNum(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_attribute"), + knownvalue.ListSizeExact(3), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: test_resource_one_output.list_attribute, err: expected 3 elements for ListSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_ListNestedBlock(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_nested_block"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_ListNestedBlockPartial(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_nested_block"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 1: knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_ListNestedBlockElements(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("list_nested_block"), + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Map(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + "key2": knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Map_KnownValueWrongType(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.ListExact([]knownvalue.Check{}), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.map_attribute, err: expected \[\]any value for ListExact check, got: map\[string\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Map_KnownValueWrongValue(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value3"), + "key4": knownvalue.StringExact("value4"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.map_attribute, err: missing element key3 for MapExact check`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_MapPartial(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_MapPartial_KnownValueWrongValue(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value1"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.map_attribute, err: missing element key3 for MapPartial check`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_MapElements(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_MapElements_WrongNum(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("map_attribute"), + knownvalue.MapSizeExact(3), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: test_resource_one_output.map_attribute, err: expected 3 elements for MapSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Number(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("123", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.NumberExact(f), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Number_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("321", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + r.Test(t, r.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return testProvider(), nil + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("int_attribute"), + knownvalue.NumberExact(f), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: test_resource_one_output.int_attribute, err: expected value 321 for NumberExact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Set(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_Set_KnownValueWrongValue(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value3"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.set_attribute, err: missing value value3 for SetExact check`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_SetPartial(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_SetPartial_KnownValueWrongValue(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value3"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: test_resource_one_output.set_attribute, err: missing value value3 for SetPartial check`), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_SetElements(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_attribute"), + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_SetNestedBlock(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_nested_block"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_SetNestedBlockPartial(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_nested_block"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_SetNestedBlockElements(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + set_nested_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("set_nested_block"), + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_String(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + knownvalue.StringExact("str")), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_String_KnownValueWrongType(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + knownvalue.BoolExact(true)), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: test_resource_one_output.string_attribute, err: expected bool value for BoolExact check, got: string"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_String_KnownValueWrongValue(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 + }, + }, + // Prior to Terraform v1.3.0 a planned output is marked as fully unknown + // if any attribute is unknown. The id attribute within the test provider + // is unknown. + // Reference: https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md#130-september-21-2022 + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + + output test_resource_one_output { + value = test_resource.one + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValueAtPath( + "test_resource_one_output", + tfjsonpath.New("string_attribute"), + knownvalue.StringExact("rts")), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: test_resource_one_output.string_attribute, err: expected value rts for StringExact check, got: str"), + }, + }, + }) +} + +func TestExpectKnownOutputValueAtPath_CheckPlan_UnknownAttributeType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + knownValue knownvalue.Check + req plancheck.CheckPlanRequest + expectedErr error + }{ + "unrecognised-type": { + knownValue: knownvalue.Int64Exact(123), + req: plancheck.CheckPlanRequest{ + Plan: &tfjson.Plan{ + OutputChanges: map[string]*tfjson.Change{ + "float32_output": { + After: float32(123), + }, + }, + }, + }, + expectedErr: fmt.Errorf("unrecognised output type: float32, known value type is knownvalue.int64Exact\n\nThis is an error in plancheck.ExpectKnownOutputValueAtPath.\nPlease report this to the maintainers."), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + e := plancheck.ExpectKnownOutputValueAtPath("float32_output", tfjsonpath.Path{}, testCase.knownValue) + + resp := plancheck.CheckPlanResponse{} + + e.CheckPlan(context.Background(), testCase.req, &resp) + + if diff := cmp.Diff(resp.Error, testCase.expectedErr, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/plancheck/expect_known_output_value_test.go b/plancheck/expect_known_output_value_test.go new file mode 100644 index 000000000..bc296a74a --- /dev/null +++ b/plancheck/expect_known_output_value_test.go @@ -0,0 +1,1510 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck_test + +import ( + "context" + "fmt" + "math/big" + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" +) + +func TestExpectKnownOutputValue_CheckPlan_OutputNotFound(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 + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "bool_not_found", + knownvalue.BoolExact(true), + ), + }, + }, + ExpectError: regexp.MustCompile("bool_not_found - Output not found in plan OutputChanges"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_AttributeValueNull(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" {} + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.BoolExact(true), + ), + }, + }, + ExpectError: regexp.MustCompile("value is null for output at path: bool_output"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Bool(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 + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.BoolExact(true), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Bool_KnownValueWrongType(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 + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.Float64Exact(1.23), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: bool_output, err: expected json\.Number value for Float64Exact check, got: bool`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Bool_KnownValueWrongValue(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 + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.BoolExact(false), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: bool_output, err: expected value false for BoolExact check, got: true"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Float64(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.23 + } + + output float64_output { + value = test_resource.one.float_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "float64_output", + knownvalue.Float64Exact(1.23), + ), + }, + }, + }, + }, + }) +} + +// We do not need equivalent tests for Int64 and Number as they all test the same logic. +func TestExpectKnownOutputValue_CheckPlan_Float64_KnownValueWrongType(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.23 + } + + output float64_output { + value = test_resource.one.float_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "float64_output", + knownvalue.StringExact("str"), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: float64_output, err: expected string value for StringExact check, got: json\.Number`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Float64_KnownValueWrongValue(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.23 + } + + output float64_output { + value = test_resource.one.float_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "float64_output", + knownvalue.Float64Exact(3.21), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: float64_output, err: expected value 3.21 for Float64Exact check, got: 1.23"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Int64(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 = 123 + } + + output int64_output { + value = test_resource.one.int_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "int64_output", + knownvalue.Int64Exact(123), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Int64_KnownValueWrongValue(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 = 123 + } + + output int64_output { + value = test_resource.one.int_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "int64_output", + knownvalue.Int64Exact(321), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: int64_output, err: expected value 321 for Int64Exact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_List(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" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_List_KnownValueWrongType(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" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_output", + knownvalue.MapExact(map[string]knownvalue.Check{}), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: list_output, err: expected map\[string\]any value for MapExact check, got: \[\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_List_KnownValueWrongValue(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" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value3"), + knownvalue.StringExact("value4"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: list_output, err: list element index 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_ListPartial(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" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }, + }) +} + +// No need to check KnownValueWrongType for ListPartial as all lists, and sets are []any in +// tfjson.Plan. +func TestExpectKnownOutputValue_CheckPlan_ListPartial_KnownValueWrongValue(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" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value3"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: list_output, err: list element 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_ListElements(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" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_ListElements_WrongNum(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" { + list_attribute = [ + "value1", + "value2" + ] + } + + output list_output { + value = test_resource.one.list_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_output", + knownvalue.ListSizeExact(3), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: list_output, err: expected 3 elements for ListSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_ListNestedBlock(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" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output list_nested_block_output { + value = test_resource.one.list_nested_block + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_nested_block_output", + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_ListNestedBlockPartial(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" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output list_nested_block_output { + value = test_resource.one.list_nested_block + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_nested_block_output", + knownvalue.ListPartial(map[int]knownvalue.Check{ + 1: knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_ListNestedBlockElements(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" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + + output list_nested_block_output { + value = test_resource.one.list_nested_block + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "list_nested_block_output", + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Map(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" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapExact(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + "key2": knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Map_KnownValueWrongType(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" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "map_output", + knownvalue.ListExact([]knownvalue.Check{}), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: map_output, err: expected \[\]any value for ListExact check, got: map\[string\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Map_KnownValueWrongValue(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" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapExact(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value3"), + "key4": knownvalue.StringExact("value4"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: map_output, err: missing element key3 for MapExact check`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_MapPartial(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" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_MapPartial_KnownValueWrongValue(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" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value1"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: map_output, err: missing element key3 for MapPartial check`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_MapElements(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" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_MapElements_WrongNum(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" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + + output map_output { + value = test_resource.one.map_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "map_output", + knownvalue.MapSizeExact(3), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: map_output, err: expected 3 elements for MapSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Number(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("123", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + 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 = 123 + } + + output int64_output { + value = test_resource.one.int_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "int64_output", + knownvalue.NumberExact(f), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Number_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("321", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + 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 = 123 + } + + output int64_output { + value = test_resource.one.int_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "int64_output", + knownvalue.NumberExact(f), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: int64_output, err: expected value 321 for NumberExact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Set(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_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_Set_KnownValueWrongValue(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_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value3"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: set_output, err: missing value value3 for SetExact check`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_SetPartial(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_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_SetPartial_KnownValueWrongValue(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_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value3"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for output at path: set_output, err: missing value value3 for SetPartial check`), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_SetElements(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_attribute = [ + "value1", + "value2" + ] + } + + output set_output { + value = test_resource.one.set_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "set_output", + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_SetNestedBlock(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_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output set_nested_block_output { + value = test_resource.one.set_nested_block + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "set_nested_block_output", + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_SetNestedBlockPartial(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_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output set_nested_block_output { + value = test_resource.one.set_nested_block + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "set_nested_block_output", + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_SetNestedBlockElements(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_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + + output set_nested_block_output { + value = test_resource.one.set_nested_block + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "set_nested_block_output", + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_String(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" + } + + output string_output { + value = test_resource.one.string_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "string_output", + knownvalue.StringExact("str")), + }, + }, + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_String_KnownValueWrongType(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" + } + + output string_output { + value = test_resource.one.string_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "string_output", + knownvalue.BoolExact(true)), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: string_output, err: expected bool value for BoolExact check, got: string"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_String_KnownValueWrongValue(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" + } + + output string_output { + value = test_resource.one.string_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "string_output", + knownvalue.StringExact("rts")), + }, + }, + ExpectError: regexp.MustCompile("error checking value for output at path: string_output, err: expected value rts for StringExact check, got: str"), + }, + }, + }) +} + +func TestExpectKnownOutputValue_CheckPlan_UnknownAttributeType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + knownValue knownvalue.Check + req plancheck.CheckPlanRequest + expectedErr error + }{ + "unrecognised-type": { + knownValue: knownvalue.Int64Exact(123), + req: plancheck.CheckPlanRequest{ + Plan: &tfjson.Plan{ + OutputChanges: map[string]*tfjson.Change{ + "float32_output": { + After: float32(123), + }, + }, + }, + }, + expectedErr: fmt.Errorf("unrecognised output type: float32, known value type is knownvalue.int64Exact\n\nThis is an error in plancheck.ExpectKnownOutputValue.\nPlease report this to the maintainers."), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + e := plancheck.ExpectKnownOutputValue("float32_output", testCase.knownValue) + + resp := plancheck.CheckPlanResponse{} + + e.CheckPlan(context.Background(), testCase.req, &resp) + + if diff := cmp.Diff(resp.Error, testCase.expectedErr, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/plancheck/expect_known_value.go b/plancheck/expect_known_value.go new file mode 100644 index 000000000..1118b9a61 --- /dev/null +++ b/plancheck/expect_known_value.go @@ -0,0 +1,86 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck + +import ( + "context" + "fmt" + "reflect" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// Resource Plan Check +var _ PlanCheck = expectKnownValue{} + +type expectKnownValue struct { + resourceAddress string + attributePath tfjsonpath.Path + knownValue knownvalue.Check +} + +// CheckPlan implements the plan check logic. +func (e expectKnownValue) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { + var rc *tfjson.ResourceChange + + for _, resourceChange := range req.Plan.ResourceChanges { + if e.resourceAddress == resourceChange.Address { + rc = resourceChange + + break + } + } + + if rc == nil { + resp.Error = fmt.Errorf("%s - Resource not found in plan ResourceChanges", e.resourceAddress) + + return + } + + result, err := tfjsonpath.Traverse(rc.Change.After, e.attributePath) + + if err != nil { + resp.Error = err + + return + } + + if result == nil { + resp.Error = fmt.Errorf("value is null for attribute at path: %s.%s", e.resourceAddress, e.attributePath.String()) + + return + } + + switch reflect.TypeOf(result).Kind() { + case reflect.Bool, + reflect.Map, + reflect.Slice, + reflect.String: + if err := e.knownValue.CheckValue(result); err != nil { + resp.Error = fmt.Errorf("error checking value for attribute at path: %s.%s, err: %s", e.resourceAddress, e.attributePath.String(), err) + + return + } + default: + errorStr := fmt.Sprintf("unrecognised attribute type: %T, known value type is %T", result, e.knownValue) + errorStr += "\n\nThis is an error in plancheck.ExpectKnownValue.\nPlease report this to the maintainers." + + resp.Error = fmt.Errorf(errorStr) + + return + } +} + +// ExpectKnownValue returns a plan check that asserts that the specified attribute at the given resource +// has a known type and value. +func ExpectKnownValue(resourceAddress string, attributePath tfjsonpath.Path, knownValue knownvalue.Check) PlanCheck { + return expectKnownValue{ + resourceAddress: resourceAddress, + attributePath: attributePath, + knownValue: knownValue, + } +} diff --git a/plancheck/expect_known_value_test.go b/plancheck/expect_known_value_test.go new file mode 100644 index 000000000..dc061572e --- /dev/null +++ b/plancheck/expect_known_value_test.go @@ -0,0 +1,1404 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck_test + +import ( + "context" + "fmt" + "math/big" + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestExpectKnownValue_CheckPlan_ResourceNotFound(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 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.two", + tfjsonpath.New("bool_attribute"), + knownvalue.BoolExact(true), + ), + }, + }, + ExpectError: regexp.MustCompile("test_resource.two - Resource not found in plan ResourceChanges"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_AttributeValueNull(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" {}`, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.BoolExact(true), + ), + }, + }, + ExpectError: regexp.MustCompile("value is null for attribute at path: test_resource.one.bool_attribute"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Bool(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 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.BoolExact(true), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Bool_KnownValueWrongType(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 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.Float64Exact(1.23), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.bool_attribute, err: expected json\.Number value for Float64Exact check, got: bool`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Bool_KnownValueWrongValue(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 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.BoolExact(false), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for attribute at path: test_resource.one.bool_attribute, err: expected value false for BoolExact check, got: true"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Float64(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.23 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("float_attribute"), + knownvalue.Float64Exact(1.23), + ), + }, + }, + }, + }, + }) +} + +// We do not need equivalent tests for Int64 and Number as they all test the same logic. +func TestExpectKnownValue_CheckPlan_Float64_KnownValueWrongType(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.23 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("float_attribute"), + knownvalue.StringExact("str"), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.float_attribute, err: expected string value for StringExact check, got: json\.Number`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Float64_KnownValueWrongValue(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.23 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("float_attribute"), + knownvalue.Float64Exact(3.21), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for attribute at path: test_resource.one.float_attribute, err: expected value 3.21 for Float64Exact check, got: 1.23"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Int64(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 = 123 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.Int64Exact(123), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Int64_KnownValueWrongValue(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 = 123 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.Int64Exact(321), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for attribute at path: test_resource.one.int_attribute, err: expected value 321 for Int64Exact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_List(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" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_List_KnownValueWrongType(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" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{}), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.list_attribute, err: expected map\[string\]any value for MapExact check, got: \[\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_List_KnownValueWrongValue(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" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("value3"), + knownvalue.StringExact("value4"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.list_attribute, err: list element index 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_ListPartial(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" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }, + }) +} + +// No need to check KnownValueWrongType for ListPartial as all lists, and sets are []any in +// tfjson.Plan. +func TestExpectKnownValue_CheckPlan_ListPartial_KnownValueWrongValue(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" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.StringExact("value3"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.list_attribute, err: list element 0: expected value value3 for StringExact check, got: value1`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_ListElements(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" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_ListElements_WrongNum(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" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListSizeExact(3), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for attribute at path: test_resource.one.list_attribute, err: expected 3 elements for ListSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_ListNestedBlock(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" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_nested_block"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_ListNestedBlockPartial(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" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_nested_block"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 1: knownvalue.MapExact(map[string]knownvalue.Check{ + "list_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_ListNestedBlockElements(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" { + list_nested_block { + list_nested_block_attribute = "str" + } + list_nested_block { + list_nested_block_attribute = "rts" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_nested_block"), + knownvalue.ListSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Map(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" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + "key2": knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Map_KnownValueWrongType(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" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.ListExact([]knownvalue.Check{}), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.map_attribute, err: expected \[\]any value for ListExact check, got: map\[string\]interface {}`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Map_KnownValueWrongValue(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" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapExact(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value3"), + "key4": knownvalue.StringExact("value4"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.map_attribute, err: missing element key3 for MapExact check`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_MapPartial(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" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key1": knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_MapPartial_KnownValueWrongValue(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" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapPartial(map[string]knownvalue.Check{ + "key3": knownvalue.StringExact("value1"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.map_attribute, err: missing element key3 for MapPartial check`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_MapElements(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" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_MapElements_WrongNum(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" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapSizeExact(3), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for attribute at path: test_resource.one.map_attribute, err: expected 3 elements for MapSizeExact check, got 2 elements"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Number(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("123", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + 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 = 123 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.NumberExact(f), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Number_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + f, _, err := big.ParseFloat("321", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + 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 = 123 + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.NumberExact(f), + ), + }, + }, + ExpectError: regexp.MustCompile("error checking value for attribute at path: test_resource.one.int_attribute, err: expected value 321 for NumberExact check, got: 123"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Set(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_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value2"), + knownvalue.StringExact("value1"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_Set_KnownValueWrongValue(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_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("value1"), + knownvalue.StringExact("value3"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.set_attribute, err: missing value value3 for SetExact check`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_SetPartial(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_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_SetPartial_KnownValueWrongValue(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_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact("value3"), + }), + ), + }, + }, + ExpectError: regexp.MustCompile(`error checking value for attribute at path: test_resource.one.set_attribute, err: missing value value3 for SetPartial check`), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_SetElements(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_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_SetNestedBlock(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_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetExact([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("str"), + }), + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_SetNestedBlockPartial(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_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.MapExact(map[string]knownvalue.Check{ + "set_nested_block_attribute": knownvalue.StringExact("rts"), + }), + }), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_SetNestedBlockElements(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_block { + set_nested_block_attribute = "str" + } + set_nested_block { + set_nested_block_attribute = "rts" + } + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_nested_block"), + knownvalue.SetSizeExact(2), + ), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_String(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" + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.StringExact("str")), + }, + }, + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_String_KnownValueWrongType(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" + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.BoolExact(true)), + }, + }, + ExpectError: regexp.MustCompile("error checking value for attribute at path: test_resource.one.string_attribute, err: expected bool value for BoolExact check, got: string"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_String_KnownValueWrongValue(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" + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.StringExact("rts")), + }, + }, + ExpectError: regexp.MustCompile("error checking value for attribute at path: test_resource.one.string_attribute, err: expected value rts for StringExact check, got: str"), + }, + }, + }) +} + +func TestExpectKnownValue_CheckPlan_UnknownAttributeType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + knownValue knownvalue.Check + req plancheck.CheckPlanRequest + expectedErr error + }{ + "unrecognised-type": { + knownValue: knownvalue.Int64Exact(123), + req: plancheck.CheckPlanRequest{ + Plan: &tfjson.Plan{ + ResourceChanges: []*tfjson.ResourceChange{ + { + Address: "example_resource.test", + Change: &tfjson.Change{ + After: map[string]any{ + "attribute": float32(123), + }, + }, + }, + }, + }, + }, + expectedErr: fmt.Errorf("unrecognised attribute type: float32, known value type is knownvalue.int64Exact\n\nThis is an error in plancheck.ExpectKnownValue.\nPlease report this to the maintainers."), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + e := plancheck.ExpectKnownValue("example_resource.test", tfjsonpath.New("attribute"), testCase.knownValue) + + resp := plancheck.CheckPlanResponse{} + + e.CheckPlan(context.Background(), testCase.req, &resp) + + if diff := cmp.Diff(resp.Error, testCase.expectedErr, equateErrorMessage); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +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/plancheck/expect_unknown_value_test.go b/plancheck/expect_unknown_value_test.go index 31798b9d0..4a46387d2 100644 --- a/plancheck/expect_unknown_value_test.go +++ b/plancheck/expect_unknown_value_test.go @@ -287,40 +287,25 @@ func testProvider() *schema.Provider { return nil }, Schema: map[string]*schema.Schema{ - "string_attribute": { + "bool_attribute": { Optional: true, - Type: schema.TypeString, + Type: schema.TypeBool, }, - - "list_attribute": { - Type: schema.TypeList, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, + "float_attribute": { Optional: true, + Type: schema.TypeFloat, }, - "set_attribute": { - Type: schema.TypeSet, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - Optional: true, - }, - "map_attribute": { - Type: schema.TypeMap, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, + "int_attribute": { Optional: true, + Type: schema.TypeInt, }, - "root_map_attribute": { - Type: schema.TypeMap, + "list_attribute": { + Type: schema.TypeList, Elem: &schema.Schema{ Type: schema.TypeString, }, Optional: true, }, - "list_nested_block": { Type: schema.TypeList, Optional: true, @@ -333,6 +318,20 @@ func testProvider() *schema.Provider { }, }, }, + "map_attribute": { + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "set_attribute": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, "set_nested_block": { Type: schema.TypeSet, Optional: true, @@ -345,6 +344,10 @@ func testProvider() *schema.Provider { }, }, }, + "string_attribute": { + Optional: true, + Type: schema.TypeString, + }, }, }, }, diff --git a/tfjsonpath/path.go b/tfjsonpath/path.go index 980cf1fea..c29ae2608 100644 --- a/tfjsonpath/path.go +++ b/tfjsonpath/path.go @@ -74,6 +74,17 @@ func (s Path) AtMapKey(key string) Path { return s } +// String returns a string representation of the Path. +func (s Path) String() string { + var pathStr []string + + for _, step := range s.steps { + pathStr = append(pathStr, fmt.Sprintf("%v", step)) + } + + return strings.Join(pathStr, ".") +} + // Traverse returns the element found when traversing the given // object using the specified Path. The object is an unmarshalled // JSON object representing Terraform data. diff --git a/tfjsonpath/path_test.go b/tfjsonpath/path_test.go index e18448728..83c13061c 100644 --- a/tfjsonpath/path_test.go +++ b/tfjsonpath/path_test.go @@ -7,6 +7,8 @@ import ( "encoding/json" "strings" "testing" + + "github.com/google/go-cmp/cmp" ) func Test_Traverse_StringValue(t *testing.T) { @@ -488,6 +490,46 @@ func Test_Traverse_Array_ExpectError(t *testing.T) { } } +func TestPath_String(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + path Path + expected string + }{ + "slice_step": { + path: New(1), + expected: "1", + }, + "map_step": { + path: New("attr"), + expected: "attr", + }, + "slice_step_map_step": { + path: New(0).AtMapKey("attr"), + expected: "0.attr", + }, + "map_step_slice_step": { + path: New("attr").AtSliceIndex(0), + expected: "attr.0", + }, + } + + for name, tc := range testCases { + name, tc := name, tc + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tc.path.String() + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func createTestObject() any { var jsonObject any jsonstring := diff --git a/website/data/plugin-testing-nav-data.json b/website/data/plugin-testing-nav-data.json index 72d37fdcb..7e581b6c3 100644 --- a/website/data/plugin-testing-nav-data.json +++ b/website/data/plugin-testing-nav-data.json @@ -50,6 +50,55 @@ } ] }, + { + "title": "Known Value Checks", + "routes": [ + { + "title": "Overview", + "path": "acceptance-tests/known-value-checks" + }, + { + "title": "Bool", + "path": "acceptance-tests/known-value-checks/bool" + }, + { + "title": "Custom", + "path": "acceptance-tests/known-value-checks/custom" + }, + { + "title": "Float64", + "path": "acceptance-tests/known-value-checks/float64" + }, + { + "title": "Int64", + "path": "acceptance-tests/known-value-checks/int64" + }, + { + "title": "List", + "path": "acceptance-tests/known-value-checks/list" + }, + { + "title": "Map", + "path": "acceptance-tests/known-value-checks/map" + }, + { + "title": "Number", + "path": "acceptance-tests/known-value-checks/number" + }, + { + "title": "Object", + "path": "acceptance-tests/known-value-checks/object" + }, + { + "title": "Set", + "path": "acceptance-tests/known-value-checks/set" + }, + { + "title": "String", + "path": "acceptance-tests/known-value-checks/string" + } + ] + }, { "title": "Sweepers", "path": "acceptance-tests/sweepers" diff --git a/website/docs/plugin/testing/acceptance-tests/known-value-checks/bool.mdx b/website/docs/plugin/testing/acceptance-tests/known-value-checks/bool.mdx new file mode 100644 index 000000000..039bc347b --- /dev/null +++ b/website/docs/plugin/testing/acceptance-tests/known-value-checks/bool.mdx @@ -0,0 +1,44 @@ +--- +page_title: 'Plugin Development - Acceptance Testing: Known Values' +description: >- + Bool Value Checks for use with Plan Checks. +--- + +# Bool Known Value Checks + +The known value checks that are available for bool values are: + +* [BoolValueExact](/terraform/plugin/testing/acceptance-tests/known-value-checks/bool#boolvalueexact-check) + +## `BoolValueExact` Check + +The [BoolValueExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#BoolValueExact) check tests that a resource attribute, or output value has an exactly matching bool value. + +Example usage of [BoolValueExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#BoolValueExact) in an [ExpectKnownValue](/terraform/plugin/testing/acceptance-tests/plan-checks/resource) plan check. + +```go +func TestExpectKnownValue_CheckPlan_Bool(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("bool_attribute"), + knownvalue.BoolValueExact(true), + ), + }, + }, + }, + }, + }) +} +``` diff --git a/website/docs/plugin/testing/acceptance-tests/known-value-checks/custom.mdx b/website/docs/plugin/testing/acceptance-tests/known-value-checks/custom.mdx new file mode 100644 index 000000000..ee48caa62 --- /dev/null +++ b/website/docs/plugin/testing/acceptance-tests/known-value-checks/custom.mdx @@ -0,0 +1,50 @@ +--- +page_title: 'Plugin Development - Acceptance Testing: Known Values' +description: >- + Custom Value Checks for use with Plan Checks. +--- + +# Custom Known Value Checks + +Custom known value checks can be created by implementing the [knownvalue.Check](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#Check) interface. + +```go +type Check interface { + CheckValue(value any) error + String() string +} +``` + +For example, a `StringValueContains` implementation could look as follows: + +```go +var _ Check = StringValueContains{} + +type StringValueContains struct { + value string +} + +func (v StringValueContains) CheckValue(other any) error { + otherVal, ok := other.(string) + + if !ok { + return fmt.Errorf("expected string value for StringValueContains check, got: %T", other) + } + + if !strings.Contains(v.value, otherVal) { + return fmt.Errorf("expected string %q to contain %q for StringValueContains check", otherVal, v.value) + } + + return nil +} + +func (v StringValueContains) String() string { + return v.value +} + +func StringValueMatch(value string) StringValue { + return StringValueContains{ + value: value, + } +} +``` diff --git a/website/docs/plugin/testing/acceptance-tests/known-value-checks/float64.mdx b/website/docs/plugin/testing/acceptance-tests/known-value-checks/float64.mdx new file mode 100644 index 000000000..d4bacb127 --- /dev/null +++ b/website/docs/plugin/testing/acceptance-tests/known-value-checks/float64.mdx @@ -0,0 +1,44 @@ +--- +page_title: 'Plugin Development - Acceptance Testing: Known Values' +description: >- + Float64 Value Checks for use with Plan Checks. +--- + +# Float64 Known Value Checks + +The known value checks that are available for float64 values are: + +* [Float64ValueExact](/terraform/plugin/testing/acceptance-tests/known-value-checks/float64#float64valueexact-check) + +## `Float64ValueExact` Check + +The [Float64ValueExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#Float64ValueExact) check tests that a resource attribute, or output value has an exactly matching float64 value. + +Example usage of [Float64ValueExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#Float64ValueExact) in an [ExpectKnownValue](/terraform/plugin/testing/acceptance-tests/plan-checks/resource) plan check. + +```go +func TestExpectKnownValue_CheckPlan_Float64(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + float_attribute = 1.23 + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("float_attribute"), + knownvalue.Float64ValueExact(1.23), + ), + }, + }, + }, + }, + }) +} +``` 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 new file mode 100644 index 000000000..dae04a89d --- /dev/null +++ b/website/docs/plugin/testing/acceptance-tests/known-value-checks/index.mdx @@ -0,0 +1,50 @@ +--- +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 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. + +## 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. + +## Using a Known Value Check + +The known value check types are implemented within the `terraform-plugin-testing` module in the [`knownvalue` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue). Known value checks are instantiated by calling the relevant constructor function. + +```go +knownvalue.BoolValueExact(true) +``` + +For known value checks that represent collections, or objects, nesting of known value checks can be used to define a "composite" known value check for use in asserting against a resource attribute, or output value that contains other values. + +```go +knownvalue.ListValueExact([]knownvalue.Check{ + knownvalue.StringValueExact("value1"), + knownvalue.StringValueExact("value2"), +}) +``` + +## Known Value Check Types + +The following table shows the correspondence between [knownvalue.Check](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#Check) types, and attributes. + +| Known Value Check Type | Framework Attribute Type | SDKv2 Attribute Type | +|-----------------------------------------------------------------------------------------------------|---------------------------|----------------------| +| [Bool Known Value Checks](/terraform/plugin/testing/acceptance-tests/known-value-checks/bool) | `schema.BoolAttribute` | `schema.TypeBool` | +| [Float64 Known Value Checks](/terraform/plugin/testing/acceptance-tests/known-value-checks/float64) | `schema.Float64Attribute` | `schema.TypeFloat` | +| [Int64 Known Value Checks](/terraform/plugin/testing/acceptance-tests/known-value-checks/int64) | `schema.Int64Attribute` | `schema.TypeInt` | +| [List Known Value Checks](/terraform/plugin/testing/acceptance-tests/known-value-checks/list) | `schema.ListAttribute` | `schema.TypeList` | +| [Map Known Value Checks](/terraform/plugin/testing/acceptance-tests/known-value-checks/map) | `schema.MapAttribute` | `schema.TypeMap` | +| [Number Known Value Checks](/terraform/plugin/testing/acceptance-tests/known-value-checks/number) | `schema.NumberAttribute` | N/A | +| [Object Known Value Checks](/terraform/plugin/testing/acceptance-tests/known-value-checks/object) | `schema.ObjectAttribute` | N/A | +| [Set Known Value Checks](/terraform/plugin/testing/acceptance-tests/known-value-checks/set) | `schema.SetAttribute` | `schema.TypeSet` | +| [String Known Value Checks](/terraform/plugin/testing/acceptance-tests/known-value-checks/string) | `schema.StringAttribute` | `schema.TypeString` | + diff --git a/website/docs/plugin/testing/acceptance-tests/known-value-checks/int64.mdx b/website/docs/plugin/testing/acceptance-tests/known-value-checks/int64.mdx new file mode 100644 index 000000000..9954cd7ec --- /dev/null +++ b/website/docs/plugin/testing/acceptance-tests/known-value-checks/int64.mdx @@ -0,0 +1,44 @@ +--- +page_title: 'Plugin Development - Acceptance Testing: Known Values' +description: >- + Int64 Value Checks for use with Plan Checks. +--- + +# Int64 Known Value Checks + +The known value checks that are available for int64 values are: + +* [Int64ValueExact](/terraform/plugin/testing/acceptance-tests/known-value-checks/float64#int64valueexact-check) + +## `Int64ValueExact` Check + +The [Int64ValueExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#Int64ValueExact) check tests that a resource attribute, or output value has an exactly matching int64 value. + +Example usage of [Int64ValueExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#Int64ValueExact) in an [ExpectKnownValue](/terraform/plugin/testing/acceptance-tests/plan-checks/resource) plan check. + +```go +func TestExpectKnownValue_CheckPlan_Int64(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + int_attribute = 123 + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("int_attribute"), + knownvalue.Int64ValueExact(123), + ), + }, + }, + }, + }, + }) +} +``` diff --git a/website/docs/plugin/testing/acceptance-tests/known-value-checks/list.mdx b/website/docs/plugin/testing/acceptance-tests/known-value-checks/list.mdx new file mode 100644 index 000000000..774cedc02 --- /dev/null +++ b/website/docs/plugin/testing/acceptance-tests/known-value-checks/list.mdx @@ -0,0 +1,126 @@ +--- +page_title: 'Plugin Development - Acceptance Testing: Known Values' +description: >- + List Value Checks for use with Plan Checks. +--- + +# List Known Value Checks + +The known value checks that are available for list values are: + +* [ListElementsExact](/terraform/plugin/testing/acceptance-tests/known-value-checks/list#listelementsexact-check) +* [ListValueExact](/terraform/plugin/testing/acceptance-tests/known-value-checks/list#listvalueexact-check) +* [ListValuePartialMatch](/terraform/plugin/testing/acceptance-tests/known-value-checks/list#listvaluepartialmatch-check) + +## `ListElementsExact` Check + +The [ListElementsExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#ListElementsExact) check tests that a resource attribute, or output value contains the specified number of elements. + +Example usage of [ListElementsExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#ListElementsExact) in an [ExpectKnownValue](/terraform/plugin/testing/acceptance-tests/plan-checks/resource) plan check. + +```go +func TestExpectKnownValue_CheckPlan_ListElements(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListElementsExact(2), + ), + }, + }, + }, + }, + }) +} +``` + +## `ListValueExact` Check + +The [ListValueExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#ListValueExact) check tests that a resource attribute, or output value has an order-dependent, matching collection of element values. + +Example usage of [ListValueExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#ListValueExact) in an [ExpectKnownValue](/terraform/plugin/testing/acceptance-tests/plan-checks/resource) plan check. + +```go +func TestExpectKnownValue_CheckPlan_List(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListValueExact([]knownvalue.Check{ + knownvalue.StringValueExact("value1"), + knownvalue.StringValueExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} +``` + +## `ListValuePartialMatch` Check + +The [ListValuePartialMatch](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#ListValuePartialMatch) check tests that a resource attribute, or output value has matching element values for the specified collection indices. + +Example usage of [ListValuePartialMatch](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#ListValuePartialMatch) in an [ExpectKnownValue](/terraform/plugin/testing/acceptance-tests/plan-checks/resource) plan check. In this example, only the first element within the list, the element defined at index `0`, is checked. + +```go +func TestExpectKnownValue_CheckPlan_ListPartial(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + list_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("list_attribute"), + knownvalue.ListValuePartialMatch(map[int]knownvalue.Check{ + 0: knownvalue.StringValueExact("value1"), + }), + ), + }, + }, + }, + }, + }) +} +``` diff --git a/website/docs/plugin/testing/acceptance-tests/known-value-checks/map.mdx b/website/docs/plugin/testing/acceptance-tests/known-value-checks/map.mdx new file mode 100644 index 000000000..ea1a5c252 --- /dev/null +++ b/website/docs/plugin/testing/acceptance-tests/known-value-checks/map.mdx @@ -0,0 +1,128 @@ +--- +page_title: 'Plugin Development - Acceptance Testing: Known Values' +description: >- + Map Value Checks for use with Plan Checks. +--- + +# Map Known Value Checks + +The known value checks that are available for map values are: + +* [MapElementsExact](/terraform/plugin/testing/acceptance-tests/known-value-checks/map#mapelementsexact-check) +* [MapValueExact](/terraform/plugin/testing/acceptance-tests/known-value-checks/map#mapvalueexact-check) +* [MapValuePartialMatch](/terraform/plugin/testing/acceptance-tests/known-value-checks/map#mapvaluepartialmatch-check) + +## `MapElementsExact` Check + +The [MapElementsExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#MapElementsExact) check tests that a resource attribute, or output value contains the specified number of elements. + +Example usage of [MapElementsExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#MapElementsExact) in an [ExpectKnownValue](/terraform/plugin/testing/acceptance-tests/plan-checks/resource) plan check. + +```go +func TestExpectKnownValue_CheckPlan_MapElements(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapElementsExact(2), + ), + }, + }, + }, + }, + }) +} +``` + +## `MapValueExact` Check + +The [MapValueExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#MapValueExact) check tests that a resource attribute, or output value has a key-specified, matching collection of element values. + +Example usage of [MapValueExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#MapValueExact) in an [ExpectKnownValue](/terraform/plugin/testing/acceptance-tests/plan-checks/resource) plan check. + +```go +func TestExpectKnownValue_CheckPlan_Map(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapValueExact(map[string]knownvalue.Check{ + "key1": knownvalue.StringValueExact("value1"), + "key2": knownvalue.StringValueExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} +``` + +## `MapValuePartialMatch` Check + +The [MapValuePartialMatch](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#MapValuePartialMatch) check tests that a resource attribute, or output value has matching element values for the specified keys. + +Example usage of [MapValuePartialMatch](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#MapValuePartialMatch) in an [ExpectKnownValue](/terraform/plugin/testing/acceptance-tests/plan-checks/resource) plan check. + +In this example, only the element associated with `key1` within the map is checked. + +```go +func TestExpectKnownValue_CheckPlan_MapPartial(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + map_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("map_attribute"), + knownvalue.MapValuePartialMatch(map[string]knownvalue.Check{ + "key1": knownvalue.StringValueExact("value1"), + }), + ), + }, + }, + }, + }, + }) +} +``` diff --git a/website/docs/plugin/testing/acceptance-tests/known-value-checks/number.mdx b/website/docs/plugin/testing/acceptance-tests/known-value-checks/number.mdx new file mode 100644 index 000000000..784146c06 --- /dev/null +++ b/website/docs/plugin/testing/acceptance-tests/known-value-checks/number.mdx @@ -0,0 +1,50 @@ +--- +page_title: 'Plugin Development - Acceptance Testing: Known Values' +description: >- + Number Value Checks for use with Plan Checks. +--- + +# Number Known Value Checks + +The known value checks that are available for number values are: + +* [NumberValueExact](/terraform/plugin/testing/acceptance-tests/known-value-checks/number#numbervalueexact-check) + +## `NumberValueExact` Check + +The [NumberValueExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#NumberValueExact) check tests that a resource attribute, or output value has an exactly matching number value. + +Example usage of [NumberValueExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#NumberValueExact) in an [ExpectKnownValue](/terraform/plugin/testing/acceptance-tests/plan-checks/resource) plan check. + +```go +func TestExpectKnownValue_CheckPlan_Number(t *testing.T) { + t.Parallel() + + num, _, err := big.ParseFloat("1.797693134862315797693134862315797693134862315", 10, 512, big.ToNearestEven) + + if err != nil { + t.Errorf("%s", err) + } + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + number_attribute = 123 + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("number_attribute"), + knownvalue.NumberValueExact(num), + ), + }, + }, + }, + }, + }) +} +``` diff --git a/website/docs/plugin/testing/acceptance-tests/known-value-checks/object.mdx b/website/docs/plugin/testing/acceptance-tests/known-value-checks/object.mdx new file mode 100644 index 000000000..55e4b923b --- /dev/null +++ b/website/docs/plugin/testing/acceptance-tests/known-value-checks/object.mdx @@ -0,0 +1,128 @@ +--- +page_title: 'Plugin Development - Acceptance Testing: Known Values' +description: >- + Object Value Checks for use with Plan Checks. +--- + +# Object Known Value Checks + +The known value checks that are available for object values are: + +* [ObjectElementsExact](/terraform/plugin/testing/acceptance-tests/known-value-checks/object#objectelementsexact-check) +* [ObjectValueExact](/terraform/plugin/testing/acceptance-tests/known-value-checks/object#objectvalueexact-check) +* [ObjectValuePartialMatch](/terraform/plugin/testing/acceptance-tests/known-value-checks/object#objectvaluepartialmatch-check) + +## `ObjectElementsExact` Check + +The [ObjectElementsExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#ObjectElementsExact) check tests that a resource attribute, or output value contains the specified number of attributes. + +Example usage of [ObjectElementsExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#ObjectElementsExact) in an [ExpectKnownValue](/terraform/plugin/testing/acceptance-tests/plan-checks/resource) plan check. + +```go +func TestExpectKnownValue_CheckPlan_ObjectElements(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + object_attribute = { + key1 = "value1" + key2 = "value2" + } + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("object_attribute"), + knownvalue.ObjectElementsExact(2), + ), + }, + }, + }, + }, + }) +} +``` + +## `ObjectValueExact` Check + +The [ObjectValueExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#ObjectValueExact) check tests that a resource attribute, or output value has a matching collection of attribute name, and attribute values. + +Example usage of [ObjectValueExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#ObjectValueExact) in an [ExpectKnownValue](/terraform/plugin/testing/acceptance-tests/plan-checks/resource) plan check. + +```go +func TestExpectKnownValue_CheckPlan_Object(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + object_attribute = { + attr1 = "value1" + attr2 = "value2" + } + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("object_attribute"), + knownvalue.ObjectValueExact(map[string]knownvalue.Check{ + "attr1": knownvalue.StringValueExact("value1"), + "attr2": knownvalue.StringValueExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} +``` + +## `ObjectValuePartialMatch` Check + +The [ObjectValuePartialMatch](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#ObjectValuePartialMatch) check tests that a resource attribute, or output value has matching attribute values for the specified attribute names. + +Example usage of [ObjectValuePartialMatch](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#ObjectValuePartialMatch) in an [ExpectKnownValue](/terraform/plugin/testing/acceptance-tests/plan-checks/resource) plan check. + +In this example, only the attribute value associated with the attribute name `attr1` within the object is checked. + +```go +func TestExpectKnownValue_CheckPlan_ObjectPartial(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + object_attribute = { + attr1 = "value1" + attr2 = "value2" + } + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("object_attribute"), + knownvalue.ObjectValuePartialMatch(map[string]knownvalue.Check{ + "attr1": knownvalue.StringValueExact("value1"), + }), + ), + }, + }, + }, + }, + }) +} +``` diff --git a/website/docs/plugin/testing/acceptance-tests/known-value-checks/set.mdx b/website/docs/plugin/testing/acceptance-tests/known-value-checks/set.mdx new file mode 100644 index 000000000..64d646a2b --- /dev/null +++ b/website/docs/plugin/testing/acceptance-tests/known-value-checks/set.mdx @@ -0,0 +1,126 @@ +--- +page_title: 'Plugin Development - Acceptance Testing: Known Values' +description: >- + Set Value Checks for use with Plan Checks. +--- + +# Set Known Value Checks + +The known value checks that are available for set values are: + +* [SetElementsExact](/terraform/plugin/testing/acceptance-tests/known-value-checks/set#setelementsexact-check) +* [SetValueExact](/terraform/plugin/testing/acceptance-tests/known-value-checks/set#setvalueexact-check) +* [SetValuePartialMatch](/terraform/plugin/testing/acceptance-tests/known-value-checks/set#setvaluepartialmatch-check) + +## `SetElementsExact` Check + +The [SetElementsExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#SetElementsExact) check tests that a resource attribute, or output value contains the specified number of elements. + +Example usage of [SetElementsExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#SetElementsExact) in an [ExpectKnownValue](/terraform/plugin/testing/acceptance-tests/plan-checks/resource) plan check. + +```go +func TestExpectKnownValue_CheckPlan_SetElements(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetElementsExact(2), + ), + }, + }, + }, + }, + }) +} +``` + +## `SetValueExact` Check + +The [SetValueExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#SetValueExact) check tests that a resource attribute, or output value has an order-independent, matching collection of element values. + +Example usage of [SetValueExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#SetValueExact) in an [ExpectKnownValue](/terraform/plugin/testing/acceptance-tests/plan-checks/resource) plan check. + +```go +func TestExpectKnownValue_CheckPlan_Set(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetValueExact([]knownvalue.Check{ + knownvalue.StringValueExact("value2"), + knownvalue.StringValueExact("value1"), + }), + ), + }, + }, + }, + }, + }) +} +``` + +## `SetValuePartialMatch` Check + +The [SetValuePartialMatch](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#SetValuePartialMatch) check tests that a resource attribute, or output value contains matching element values. + +Example usage of [SetValuePartialMatch](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#SetValuePartialMatch) in an [ExpectKnownValue](/terraform/plugin/testing/acceptance-tests/plan-checks/resource) plan check. In this example, only the one element within the set is checked. + +```go +func TestExpectKnownValue_CheckPlan_SetPartial(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + set_attribute = [ + "value1", + "value2" + ] + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("set_attribute"), + knownvalue.SetValuePartialMatch([]knownvalue.Check{ + knownvalue.StringValueExact("value2"), + }), + ), + }, + }, + }, + }, + }) +} +``` diff --git a/website/docs/plugin/testing/acceptance-tests/known-value-checks/string.mdx b/website/docs/plugin/testing/acceptance-tests/known-value-checks/string.mdx new file mode 100644 index 000000000..2d771964b --- /dev/null +++ b/website/docs/plugin/testing/acceptance-tests/known-value-checks/string.mdx @@ -0,0 +1,43 @@ +--- +page_title: 'Plugin Development - Acceptance Testing: Known Values' +description: >- + String Value Checks for use with Plan Checks. +--- + +# String Known Value Checks + +The known value checks that are available for string values are: + +* [StringValueExact](/terraform/plugin/testing/acceptance-tests/known-value-checks/string#stringvalueexact-check) + +## `StringValueExact` Check + +The [StringValueExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#StringValueExact) check tests that a resource attribute, or output value has an exactly matching string value. + +Example usage of [StringValueExact](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#StringValueExact) in an [ExpectKnownValue](/terraform/plugin/testing/acceptance-tests/plan-checks/resource) plan check. + +```go +func TestExpectKnownValue_CheckPlan_String(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Provider definition omitted. + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.StringValueExact("str")), + }, + }, + }, + }, + }) +} +``` diff --git a/website/docs/plugin/testing/acceptance-tests/plan-checks/output.mdx b/website/docs/plugin/testing/acceptance-tests/plan-checks/output.mdx index 8e8edd1bd..146ea2de7 100644 --- a/website/docs/plugin/testing/acceptance-tests/plan-checks/output.mdx +++ b/website/docs/plugin/testing/acceptance-tests/plan-checks/output.mdx @@ -9,12 +9,86 @@ description: >- The `terraform-plugin-testing` module provides a package [`plancheck`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck) with built-in output value plan checks for common use-cases: -| Check | Description | -|------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------| -| [`plancheck.ExpectNullOutputValue(address)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectNullOutputValue) | Asserts the output at the specified address has a null value. | -| [`plancheck.ExpectNullOutputValueAtPath(address, path)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectNullOutputValueAtPath) | Asserts the output at the specified address, and path has a null value. | -| [`plancheck.ExpectUnknownOutputValue(address)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectUnknownOutputValue) | Asserts the output at the specified address has an unknown value. | -| [`plancheck.ExpectUnknownOutputValueAtPath(address, path)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectUnknownOutputValueAtPath) | Asserts the output at the specified address, and path has an unknown value. | +| Check | Description | +|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------| +| [`plancheck.ExpectKnownOutputValue(address, value)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectKnownOutputValue) | Asserts the output at the specified address has the specified type, and value. | +| [`plancheck.ExpectKnownOutputValueAtPath(address, path, value)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectKnownOutputValueAtPath) | Asserts the output at the specified address, and path has the specified type, and value. | +| [`plancheck.ExpectNullOutputValue(address)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectNullOutputValue) | Asserts the output at the specified address has a null value. | +| [`plancheck.ExpectNullOutputValueAtPath(address, path)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectNullOutputValueAtPath) | Asserts the output at the specified address, and path has a null value. | +| [`plancheck.ExpectUnknownOutputValue(address)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectUnknownOutputValue) | Asserts the output at the specified address has an unknown value. | +| [`plancheck.ExpectUnknownOutputValueAtPath(address, path)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectUnknownOutputValueAtPath) | Asserts the output at the specified address, and path has an unknown value. | + +## Example using `plancheck.ExpectKnownOutputValue` + +The [`plancheck.ExpectKnownOutputValue(address, value)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectKnownOutputValue) plan check verifies that a specific output value has a known type, and value. + +Refer to [Known Value Checks](/terraform/plugin/testing/acceptance-tests/known-value-checks) for details, and examples of the available [knownvalue.Check](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#Check) types that can be used with the `ExpectKnownOutputValue` plan check. + +```go +func TestExpectKnownOutputValue_CheckPlan_Bool(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + // Provider definition omitted. + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.BoolValueExact(true), + ), + }, + }, + }, + }, + }) +} +``` + +## Example using `plancheck.ExpectKnownOutputValueAtPath` + +The [`plancheck.ExpectKnownOutputValueAtPath(address, path, value)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectKnownOutputValueAtPath) plan check verifies that a specific output value at a defined path has a known type, and value. + +Refer to [Known Value Checks](/terraform/plugin/testing/acceptance-tests/known-value-checks) for details, and examples of the available [knownvalue.Check](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#Check) types that can be used with the `ExpectKnownOutputValueAtPath` plan check. + +```go +func TestExpectKnownOutputValue_CheckPlan_Bool(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + // Provider definition omitted. + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + bool_attribute = true + } + + output bool_output { + value = test_resource.one.bool_attribute + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownOutputValue( + "bool_output", + knownvalue.BoolValueExact(true), + ), + }, + }, + }, + }, + }) +} +``` ## Example using `plancheck.ExpectUnknownOutputValue` diff --git a/website/docs/plugin/testing/acceptance-tests/plan-checks/resource.mdx b/website/docs/plugin/testing/acceptance-tests/plan-checks/resource.mdx index 6e197e043..c0f93b6e7 100644 --- a/website/docs/plugin/testing/acceptance-tests/plan-checks/resource.mdx +++ b/website/docs/plugin/testing/acceptance-tests/plan-checks/resource.mdx @@ -9,11 +9,44 @@ description: >- The `terraform-plugin-testing` module provides a package [`plancheck`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck) with built-in managed resource, and data source plan checks for common use-cases: -| Check | Description | -|---------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------| -| [`plancheck.ExpectResourceAction(address, operation)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectResourceAction) | Asserts the given managed resource, or data source, has the specified operation for apply. | -| [`plancheck.ExpectUnknownValue(address, path)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectUnknownValue) | Asserts the specified attribute at the given managed resource, or data source, has an unknown value. | -| [`plancheck.ExpectSensitiveValue(address, path)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectSensitiveValue) | Asserts the specified attribute at the given managed resource, or data source, has a sensitive value. | +| Check | Description | +|---------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------| +| [`plancheck.ExpectKnownValue(address, path, value)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectKnownValue) | Asserts the specified attribute at the given managed resource, or data source, has the specified type, and value. | +| [`plancheck.ExpectResourceAction(address, operation)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectResourceAction) | Asserts the given managed resource, or data source, has the specified operation for apply. | +| [`plancheck.ExpectSensitiveValue(address, path)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectSensitiveValue) | Asserts the specified attribute at the given managed resource, or data source, has a sensitive value. | +| [`plancheck.ExpectUnknownValue(address, path)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectUnknownValue) | Asserts the specified attribute at the given managed resource, or data source, has an unknown value. | + +## Example using `plancheck.ExpectKnownValue` + +The [`plancheck.ExpectKnownValue(address, path, value)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#ExpectKnownValue) plan check provides a basis for asserting that a specific resource attribute has a known type, and value. + +Refer to [Known Value Checks](/terraform/plugin/testing/acceptance-tests/known-value-checks) for details, and examples of the available [knownvalue.Check](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/knownvalue#Check) types that can be used with the `ExpectKnownValue` plan check. + +```go +func TestExpectKnownValue_CheckPlan_String(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + // Provider definition omitted. + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "one" { + string_attribute = "str" + } + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "test_resource.one", + tfjsonpath.New("string_attribute"), + knownvalue.StringValueExact("str")), + }, + }, + }, + }, + }) +} +``` ## Examples using `plancheck.ExpectResourceAction`