Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/unreleased/FEATURES-20250513-115526.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: FEATURES
body: 'statecheck: Added `ExpectIdentityValueMatchesState` state check to assert that an identity value matches a state value at the same path.'
time: 2025-05-13T11:55:26.406171-04:00
custom:
Issue: "503"
5 changes: 5 additions & 0 deletions .changes/unreleased/FEATURES-20250514-095016.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: FEATURES
body: 'statecheck: Added `ExpectIdentityValueMatchesStateAtPath` state check to assert that an identity value matches a state value at different paths.'
time: 2025-05-14T09:50:16.101201-04:00
custom:
Issue: "503"
Original file line number Diff line number Diff line change
Expand Up @@ -890,7 +890,7 @@ func (s ProviderServer) UpgradeResourceState(ctx context.Context, req *tfprotov6
}

func (s ProviderServer) UpgradeResourceIdentity(context.Context, *tfprotov6.UpgradeResourceIdentityRequest) (*tfprotov6.UpgradeResourceIdentityResponse, error) {
// TODO: Implement
// TODO: This isn't currently being used by the testing framework provider, so no need to implement it until then.
return nil, errors.New("UpgradeResourceIdentity is not currently implemented in testprovider")
}

Expand Down
7 changes: 0 additions & 7 deletions statecheck/expect_identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"regexp"
"testing"

"github.com/hashicorp/go-version"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"

r "github.com/hashicorp/terraform-plugin-testing/helper/resource"
Expand Down Expand Up @@ -134,12 +133,6 @@ func TestExpectIdentity_CheckState(t *testing.T) {
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(),
Expand Down
97 changes: 97 additions & 0 deletions statecheck/expect_identity_value_matches_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package statecheck

import (
"context"
"fmt"
"reflect"

tfjson "github.com/hashicorp/terraform-json"

"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
)

var _ StateCheck = expectIdentityValueMatchesState{}

type expectIdentityValueMatchesState struct {
resourceAddress string
attributePath tfjsonpath.Path
}

// CheckState implements the state check logic.
func (e expectIdentityValueMatchesState) 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
}

identityResult, err := tfjsonpath.Traverse(resource.IdentityValues, e.attributePath)

if err != nil {
resp.Error = err

return
}

stateResult, err := tfjsonpath.Traverse(resource.AttributeValues, e.attributePath)

if err != nil {
resp.Error = err

return
}

if !reflect.DeepEqual(identityResult, stateResult) {
resp.Error = fmt.Errorf("expected identity and state value at path to match, but they differ: %s.%s, identity value: %v, state value: %v", e.resourceAddress, e.attributePath.String(), identityResult, stateResult)

return
}
}

// ExpectIdentityValueMatchesState returns a state check that asserts that the specified identity attribute at the given resource
// matches the same attribute in state. This is useful when an identity attribute is in sync with a state attribute of the same path.
//
// This state check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+
func ExpectIdentityValueMatchesState(resourceAddress string, attributePath tfjsonpath.Path) StateCheck {
return expectIdentityValueMatchesState{
resourceAddress: resourceAddress,
attributePath: attributePath,
}
}
106 changes: 106 additions & 0 deletions statecheck/expect_identity_value_matches_state_at_path.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package statecheck

import (
"context"
"fmt"
"reflect"

tfjson "github.com/hashicorp/terraform-json"

"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
)

var _ StateCheck = expectIdentityValueMatchesStateAtPath{}

type expectIdentityValueMatchesStateAtPath struct {
resourceAddress string
identityAttrPath tfjsonpath.Path
stateAttrPath tfjsonpath.Path
}

// CheckState implements the state check logic.
func (e expectIdentityValueMatchesStateAtPath) 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
}

identityResult, err := tfjsonpath.Traverse(resource.IdentityValues, e.identityAttrPath)

if err != nil {
resp.Error = err

return
}

stateResult, err := tfjsonpath.Traverse(resource.AttributeValues, e.stateAttrPath)

if err != nil {
resp.Error = err

return
}

if !reflect.DeepEqual(identityResult, stateResult) {
resp.Error = fmt.Errorf(
"expected identity (%[1]s.%[2]s) and state value (%[1]s.%[3]s) to match, but they differ: identity value: %[4]v, state value: %[5]v",
e.resourceAddress,
e.identityAttrPath.String(),
e.stateAttrPath.String(),
identityResult,
stateResult,
)

return
}
}

// ExpectIdentityValueMatchesStateAtPath returns a state check that asserts that the specified identity attribute at the given resource
// matches the specified attribute in state. This is useful when an identity attribute is in sync with a state attribute of a different path.
//
// This state check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+
func ExpectIdentityValueMatchesStateAtPath(resourceAddress string, identityAttrPath, stateAttrPath tfjsonpath.Path) StateCheck {
return expectIdentityValueMatchesStateAtPath{
resourceAddress: resourceAddress,
identityAttrPath: identityAttrPath,
stateAttrPath: stateAttrPath,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// 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/statecheck"
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
"github.com/hashicorp/terraform-plugin-testing/tfversion"
)

func ExampleExpectIdentityValueMatchesStateAtPath() {
// 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 an "identity_id" string attribute
// - Has a resource schema with an "state_id" string attribute
Steps: []resource.TestStep{
{
Config: `resource "test_resource" "one" {}`,
ConfigStateChecks: []statecheck.StateCheck{
// The identity attribute at "identity_id" and state attribute at "state_id" must match
statecheck.ExpectIdentityValueMatchesStateAtPath(
"test_resource.one",
tfjsonpath.New("identity_id"),
tfjsonpath.New("state_id"),
),
},
},
},
})
}
Loading
Loading