diff --git a/.changes/unreleased/NOTES-20250325-115927.yaml b/.changes/unreleased/NOTES-20250325-115927.yaml index 3f48414f8..73ab4f880 100644 --- a/.changes/unreleased/NOTES-20250325-115927.yaml +++ b/.changes/unreleased/NOTES-20250325-115927.yaml @@ -1,7 +1,7 @@ kind: NOTES body: This alpha pre-release contains testing utilities for managed resource identity, which can be used with `Terraform v1.12.0-alpha20250319`, to assert identity data stored during apply workflows. A managed resource in a provider can read/store identity data using the `terraform-plugin-framework@v1.15.0-alpha.1` - or `terraform-plugin-sdk/v2@v2.37.0-alpha.1` Go modules. To assert identity data stored by a provider in state, use the `statecheck.ExpectIdentityValue` state check. + or `terraform-plugin-sdk/v2@v2.37.0-alpha.1` Go modules. To assert identity data stored by a provider in state, use the `statecheck.ExpectIdentity` state check. time: 2025-03-25T11:59:27.455519-04:00 custom: - Issue: "468" + Issue: "470" diff --git a/.changes/unreleased/upcoming-stable/ENHANCEMENTS-20250325-121007.yaml b/.changes/unreleased/upcoming-stable/ENHANCEMENTS-20250325-121007.yaml index c73a60ef1..654a44b72 100644 --- a/.changes/unreleased/upcoming-stable/ENHANCEMENTS-20250325-121007.yaml +++ b/.changes/unreleased/upcoming-stable/ENHANCEMENTS-20250325-121007.yaml @@ -1,5 +1,5 @@ kind: ENHANCEMENTS -body: 'statecheck: Added `ExpectIdentityValue` state check, which asserts managed resource identity data stored in state.' +body: 'statecheck: Added `ExpectIdentityValue` state check, which asserts a specified attribute value of a managed resource identity in state.' time: 2025-03-25T12:10:07.55484-04:00 custom: Issue: "468" diff --git a/.changes/unreleased/upcoming-stable/ENHANCEMENTS-20250325-174504.yaml b/.changes/unreleased/upcoming-stable/ENHANCEMENTS-20250325-174504.yaml new file mode 100644 index 000000000..3b60c561f --- /dev/null +++ b/.changes/unreleased/upcoming-stable/ENHANCEMENTS-20250325-174504.yaml @@ -0,0 +1,5 @@ +kind: ENHANCEMENTS +body: 'statecheck: Added `ExpectIdentity` state check, which asserts all data of a managed resource identity in state.' +time: 2025-03-25T17:45:04.794886-04:00 +custom: + Issue: "470" diff --git a/go.mod b/go.mod index b240f9d94..d4c088650 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.22.0 github.com/hashicorp/terraform-json v0.24.1-0.20250314103308-f86d5e36f4ab - github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1 + github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1.0.20250325210248-fa8d1fe4306b github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 github.com/mitchellh/go-testing-interface v1.14.1 @@ -58,5 +58,5 @@ require ( google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.71.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect + google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index d9986ce44..831040ddf 100644 --- a/go.sum +++ b/go.sum @@ -80,8 +80,8 @@ github.com/hashicorp/terraform-exec v0.22.0 h1:G5+4Sz6jYZfRYUCg6eQgDsqTzkNXV+fP8 github.com/hashicorp/terraform-exec v0.22.0/go.mod h1:bjVbsncaeh8jVdhttWYZuBGj21FcYw6Ia/XfHcNO7lQ= github.com/hashicorp/terraform-json v0.24.1-0.20250314103308-f86d5e36f4ab h1:5Qpuprk76zkVEdTCtfoPjUc+1AeUxlgkF6sWTr7qLDs= github.com/hashicorp/terraform-json v0.24.1-0.20250314103308-f86d5e36f4ab/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc= -github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1 h1:/IZFNUEafGnJGXRe2iNQQ+vtzEw/5qiD+gOxkFrNbi4= -github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1/go.mod h1:Tf2HngbyKvovAlGXgBOVGm3EDvbNaN/StUaTXwrej4o= +github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1.0.20250325210248-fa8d1fe4306b h1:JCAO+OdLztQ6F2bZ8lU93u986UVQl2Y/HNz18/jg3b0= +github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1.0.20250325210248-fa8d1fe4306b/go.mod h1:HFPb73wivXPZy5wMuE7T3WqFbpIj6R6q1svKnZsnMZo= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 h1:WNMsTLkZf/3ydlgsuXePa3jvZFwAJhruxTxP/c1Viuw= @@ -220,8 +220,8 @@ google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/testing/testsdk/providerserver/tftypes.go b/internal/testing/testsdk/providerserver/tftypes.go index e34541d35..a15ba1143 100644 --- a/internal/testing/testsdk/providerserver/tftypes.go +++ b/internal/testing/testsdk/providerserver/tftypes.go @@ -76,10 +76,10 @@ func IdentityDynamicValueToValue(schema *tfprotov6.ResourceIdentitySchema, dynam } if dynamicValue == nil { - return tftypes.NewValue(getIdentitySchemaValueType(schema), nil), nil + return tftypes.NewValue(schema.ValueType(), nil), nil } - value, err := dynamicValue.Unmarshal(getIdentitySchemaValueType(schema)) + value, err := dynamicValue.Unmarshal(schema.ValueType()) if err != nil { diag := &tfprotov6.Diagnostic{ @@ -105,7 +105,7 @@ func IdentityValuetoDynamicValue(schema *tfprotov6.ResourceIdentitySchema, value return nil, diag } - dynamicValue, err := tfprotov6.NewDynamicValue(getIdentitySchemaValueType(schema), value) + dynamicValue, err := tfprotov6.NewDynamicValue(schema.ValueType(), value) if err != nil { diag := &tfprotov6.Diagnostic{ @@ -119,42 +119,3 @@ func IdentityValuetoDynamicValue(schema *tfprotov6.ResourceIdentitySchema, value return &dynamicValue, nil } - -// TODO: This should be replaced by the `ValueType` method from plugin-go: -// https://github.com/hashicorp/terraform-plugin-go/pull/497 -func getIdentitySchemaValueType(schema *tfprotov6.ResourceIdentitySchema) tftypes.Type { - if schema == nil || schema.IdentityAttributes == nil { - return tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, - } - } - attributeTypes := map[string]tftypes.Type{} - - for _, attribute := range schema.IdentityAttributes { - if attribute == nil { - continue - } - - attributeType := getIdentityAttributeValueType(attribute) - - if attributeType == nil { - continue - } - - attributeTypes[attribute.Name] = attributeType - } - - return tftypes.Object{ - AttributeTypes: attributeTypes, - } -} - -// TODO: This should be replaced by the `ValueType` method from plugin-go: -// https://github.com/hashicorp/terraform-plugin-go/pull/497 -func getIdentityAttributeValueType(attr *tfprotov6.ResourceIdentitySchemaAttribute) tftypes.Type { - if attr == nil { - return nil - } - - return attr.Type -} diff --git a/knownvalue/object.go b/knownvalue/object.go index e15c03928..cc97542c4 100644 --- a/knownvalue/object.go +++ b/knownvalue/object.go @@ -106,7 +106,7 @@ func createDeltaString[T any, V any](mapA map[string]T, mapB map[string]V, msgPr for i, k := range deltaKeys { if i == 0 { deltaMsg += msgPrefix - } else if i != 0 { + } else { deltaMsg += ", " } deltaMsg += fmt.Sprintf("%q", k) diff --git a/statecheck/expect_identity.go b/statecheck/expect_identity.go new file mode 100644 index 000000000..df5147b23 --- /dev/null +++ b/statecheck/expect_identity.go @@ -0,0 +1,138 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck + +import ( + "context" + "fmt" + "maps" + "slices" + "sort" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +var _ StateCheck = expectIdentity{} + +type expectIdentity struct { + resourceAddress string + identity map[string]knownvalue.Check +} + +// CheckState implements the state check logic. +func (e expectIdentity) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) { + var resource *tfjson.StateResource + + if req.State == nil { + resp.Error = fmt.Errorf("state is nil") + + return + } + + if req.State.Values == nil { + resp.Error = fmt.Errorf("state does not contain any state values") + + return + } + + if req.State.Values.RootModule == nil { + resp.Error = fmt.Errorf("state does not contain a root module") + + return + } + + for _, r := range req.State.Values.RootModule.Resources { + if e.resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddress) + + return + } + + if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 { + resp.Error = fmt.Errorf("%s - Identity not found in state. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + + return + } + + if len(resource.IdentityValues) != len(e.identity) { + deltaMsg := "" + if len(resource.IdentityValues) > len(e.identity) { + deltaMsg = createDeltaString(resource.IdentityValues, e.identity, "actual identity has extra attribute(s): ") + } else { + deltaMsg = createDeltaString(e.identity, resource.IdentityValues, "actual identity is missing attribute(s): ") + } + + resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.resourceAddress, len(e.identity), len(resource.IdentityValues), deltaMsg) + return + } + + var keys []string + + for k := range e.identity { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + actualIdentityVal, ok := resource.IdentityValues[k] + + if !ok { + resp.Error = fmt.Errorf("%s - missing attribute %q in actual identity object", e.resourceAddress, k) + return + } + + if err := e.identity[k].CheckValue(actualIdentityVal); err != nil { + resp.Error = fmt.Errorf("%s - %q identity attribute: %s", e.resourceAddress, k, err) + return + } + } +} + +// ExpectIdentity returns a state check that asserts that the identity at the given resource matches a known object, where each +// map key represents an identity attribute name. The identity in state must exactly match the given object and any missing/extra +// attributes will raise a diagnostic. +// +// This state check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+ +func ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check) StateCheck { + return expectIdentity{ + resourceAddress: resourceAddress, + identity: identity, + } +} + +// createDeltaString prints the map keys that are present in mapA and not present in mapB +func createDeltaString[T any, V any](mapA map[string]T, mapB map[string]V, msgPrefix string) string { + deltaMsg := "" + + deltaMap := make(map[string]T, len(mapA)) + maps.Copy(deltaMap, mapA) + for key := range mapB { + delete(deltaMap, key) + } + + deltaKeys := slices.Sorted(maps.Keys(deltaMap)) + + for i, k := range deltaKeys { + if i == 0 { + deltaMsg += msgPrefix + } else { + deltaMsg += ", " + } + deltaMsg += fmt.Sprintf("%q", k) + } + + return deltaMsg +} diff --git a/statecheck/expect_identity_example_test.go b/statecheck/expect_identity_example_test.go new file mode 100644 index 000000000..1db3766c4 --- /dev/null +++ b/statecheck/expect_identity_example_test.go @@ -0,0 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func ExampleExpectIdentity() { + // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. + t := &testing.T{} + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Resource identity support is only available in Terraform v1.12+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + // Provider definition omitted. Assuming "test_resource" has an identity schema with "id" and "name" string attributes + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity( + "test_resource.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "name": knownvalue.StringExact("John Doe"), + }, + ), + }, + }, + }, + }) +} diff --git a/statecheck/expect_identity_test.go b/statecheck/expect_identity_test.go new file mode 100644 index 000000000..5a0101435 --- /dev/null +++ b/statecheck/expect_identity_test.go @@ -0,0 +1,341 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestExpectIdentity_CheckState_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity( + "examplecloud_thing.two", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in state"), + }, + }, + }) +} + +func TestExpectIdentity_CheckState_No_Terraform_Identity_Support(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + tfversion.SkipAbove(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource support identity, but the Terraform versions running will not. + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in state. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentity_CheckState_No_Identity(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource does not support identity + "examplecloud": examplecloudProviderNoIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in state. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentity_CheckState(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + // TODO: There is currently a bug in Terraform v1.12.0-alpha20250319 that causes a panic + // when refreshing a resource that has an identity stored via protocol v6. + // + // We can remove this skip once the bug fix is merged/released: + // - https://github.com/hashicorp/terraform/pull/36756 + tfversion.SkipIf(version.Must(version.NewVersion("1.12.0-alpha20250319"))), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + }, + }, + }) +} + +func TestExpectIdentity_CheckState_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.Bool(true), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - "id" identity attribute: expected bool value for Bool check, got: string`), + }, + }, + }) +} + +func TestExpectIdentity_CheckState_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("321-id"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - "id" identity attribute: expected value 321-id for StringExact check, got: id-123`), + }, + }, + }) +} + +func TestExpectIdentity_CheckState_ExtraAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("321-id"), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Expected 1 attribute\(s\) in the actual identity object, got 2 attribute\(s\): actual identity has extra attribute\(s\): "list_of_numbers"`), + }, + }, + }) +} + +func TestExpectIdentity_CheckState_MissingAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "nonexistent_attr": knownvalue.StringExact("hello"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Expected 3 attribute\(s\) in the actual identity object, got 2 attribute\(s\): actual identity is missing attribute\(s\): "nonexistent_attr"`), + }, + }, + }) +} + +func TestExpectIdentity_CheckState_MismatchedAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "not_id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - missing attribute "not_id" in actual identity object`), + }, + }, + }) +}