Skip to content

Commit 8b8c33a

Browse files
authored
statecheck: Add new resource identity / state comparison checks (#503)
* update comments * add new state check * add changelog * add `ExpectIdentityValueMatchesStateAtPath`
1 parent fc2179d commit 8b8c33a

11 files changed

+1011
-23
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: FEATURES
2+
body: 'statecheck: Added `ExpectIdentityValueMatchesState` state check to assert that an identity value matches a state value at the same path.'
3+
time: 2025-05-13T11:55:26.406171-04:00
4+
custom:
5+
Issue: "503"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: FEATURES
2+
body: 'statecheck: Added `ExpectIdentityValueMatchesStateAtPath` state check to assert that an identity value matches a state value at different paths.'
3+
time: 2025-05-14T09:50:16.101201-04:00
4+
custom:
5+
Issue: "503"

internal/testing/testsdk/providerserver/providerserver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -890,7 +890,7 @@ func (s ProviderServer) UpgradeResourceState(ctx context.Context, req *tfprotov6
890890
}
891891

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

statecheck/expect_identity_test.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"regexp"
88
"testing"
99

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

1312
r "github.com/hashicorp/terraform-plugin-testing/helper/resource"
@@ -134,12 +133,6 @@ func TestExpectIdentity_CheckState(t *testing.T) {
134133
r.Test(t, r.TestCase{
135134
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
136135
tfversion.SkipBelow(tfversion.Version1_12_0),
137-
// TODO: There is currently a bug in Terraform v1.12.0-alpha20250319 that causes a panic
138-
// when refreshing a resource that has an identity stored via protocol v6.
139-
//
140-
// We can remove this skip once the bug fix is merged/released:
141-
// - https://github.com/hashicorp/terraform/pull/36756
142-
tfversion.SkipIf(version.Must(version.NewVersion("1.12.0-alpha20250319"))),
143136
},
144137
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
145138
"examplecloud": examplecloudProviderWithResourceIdentity(),
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package statecheck
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"reflect"
10+
11+
tfjson "github.com/hashicorp/terraform-json"
12+
13+
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
14+
)
15+
16+
var _ StateCheck = expectIdentityValueMatchesState{}
17+
18+
type expectIdentityValueMatchesState struct {
19+
resourceAddress string
20+
attributePath tfjsonpath.Path
21+
}
22+
23+
// CheckState implements the state check logic.
24+
func (e expectIdentityValueMatchesState) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) {
25+
var resource *tfjson.StateResource
26+
27+
if req.State == nil {
28+
resp.Error = fmt.Errorf("state is nil")
29+
30+
return
31+
}
32+
33+
if req.State.Values == nil {
34+
resp.Error = fmt.Errorf("state does not contain any state values")
35+
36+
return
37+
}
38+
39+
if req.State.Values.RootModule == nil {
40+
resp.Error = fmt.Errorf("state does not contain a root module")
41+
42+
return
43+
}
44+
45+
for _, r := range req.State.Values.RootModule.Resources {
46+
if e.resourceAddress == r.Address {
47+
resource = r
48+
49+
break
50+
}
51+
}
52+
53+
if resource == nil {
54+
resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddress)
55+
56+
return
57+
}
58+
59+
if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 {
60+
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)
61+
62+
return
63+
}
64+
65+
identityResult, err := tfjsonpath.Traverse(resource.IdentityValues, e.attributePath)
66+
67+
if err != nil {
68+
resp.Error = err
69+
70+
return
71+
}
72+
73+
stateResult, err := tfjsonpath.Traverse(resource.AttributeValues, e.attributePath)
74+
75+
if err != nil {
76+
resp.Error = err
77+
78+
return
79+
}
80+
81+
if !reflect.DeepEqual(identityResult, stateResult) {
82+
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)
83+
84+
return
85+
}
86+
}
87+
88+
// ExpectIdentityValueMatchesState returns a state check that asserts that the specified identity attribute at the given resource
89+
// matches the same attribute in state. This is useful when an identity attribute is in sync with a state attribute of the same path.
90+
//
91+
// This state check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+
92+
func ExpectIdentityValueMatchesState(resourceAddress string, attributePath tfjsonpath.Path) StateCheck {
93+
return expectIdentityValueMatchesState{
94+
resourceAddress: resourceAddress,
95+
attributePath: attributePath,
96+
}
97+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package statecheck
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"reflect"
10+
11+
tfjson "github.com/hashicorp/terraform-json"
12+
13+
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
14+
)
15+
16+
var _ StateCheck = expectIdentityValueMatchesStateAtPath{}
17+
18+
type expectIdentityValueMatchesStateAtPath struct {
19+
resourceAddress string
20+
identityAttrPath tfjsonpath.Path
21+
stateAttrPath tfjsonpath.Path
22+
}
23+
24+
// CheckState implements the state check logic.
25+
func (e expectIdentityValueMatchesStateAtPath) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) {
26+
var resource *tfjson.StateResource
27+
28+
if req.State == nil {
29+
resp.Error = fmt.Errorf("state is nil")
30+
31+
return
32+
}
33+
34+
if req.State.Values == nil {
35+
resp.Error = fmt.Errorf("state does not contain any state values")
36+
37+
return
38+
}
39+
40+
if req.State.Values.RootModule == nil {
41+
resp.Error = fmt.Errorf("state does not contain a root module")
42+
43+
return
44+
}
45+
46+
for _, r := range req.State.Values.RootModule.Resources {
47+
if e.resourceAddress == r.Address {
48+
resource = r
49+
50+
break
51+
}
52+
}
53+
54+
if resource == nil {
55+
resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddress)
56+
57+
return
58+
}
59+
60+
if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 {
61+
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)
62+
63+
return
64+
}
65+
66+
identityResult, err := tfjsonpath.Traverse(resource.IdentityValues, e.identityAttrPath)
67+
68+
if err != nil {
69+
resp.Error = err
70+
71+
return
72+
}
73+
74+
stateResult, err := tfjsonpath.Traverse(resource.AttributeValues, e.stateAttrPath)
75+
76+
if err != nil {
77+
resp.Error = err
78+
79+
return
80+
}
81+
82+
if !reflect.DeepEqual(identityResult, stateResult) {
83+
resp.Error = fmt.Errorf(
84+
"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",
85+
e.resourceAddress,
86+
e.identityAttrPath.String(),
87+
e.stateAttrPath.String(),
88+
identityResult,
89+
stateResult,
90+
)
91+
92+
return
93+
}
94+
}
95+
96+
// ExpectIdentityValueMatchesStateAtPath returns a state check that asserts that the specified identity attribute at the given resource
97+
// matches the specified attribute in state. This is useful when an identity attribute is in sync with a state attribute of a different path.
98+
//
99+
// This state check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+
100+
func ExpectIdentityValueMatchesStateAtPath(resourceAddress string, identityAttrPath, stateAttrPath tfjsonpath.Path) StateCheck {
101+
return expectIdentityValueMatchesStateAtPath{
102+
resourceAddress: resourceAddress,
103+
identityAttrPath: identityAttrPath,
104+
stateAttrPath: stateAttrPath,
105+
}
106+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package statecheck_test
5+
6+
import (
7+
"testing"
8+
9+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
10+
"github.com/hashicorp/terraform-plugin-testing/statecheck"
11+
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
12+
"github.com/hashicorp/terraform-plugin-testing/tfversion"
13+
)
14+
15+
func ExampleExpectIdentityValueMatchesStateAtPath() {
16+
// A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`.
17+
t := &testing.T{}
18+
t.Parallel()
19+
20+
resource.Test(t, resource.TestCase{
21+
// Resource identity support is only available in Terraform v1.12+
22+
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
23+
tfversion.SkipBelow(tfversion.Version1_12_0),
24+
},
25+
// Provider definition omitted. Assuming "test_resource":
26+
// - Has an identity schema with an "identity_id" string attribute
27+
// - Has a resource schema with an "state_id" string attribute
28+
Steps: []resource.TestStep{
29+
{
30+
Config: `resource "test_resource" "one" {}`,
31+
ConfigStateChecks: []statecheck.StateCheck{
32+
// The identity attribute at "identity_id" and state attribute at "state_id" must match
33+
statecheck.ExpectIdentityValueMatchesStateAtPath(
34+
"test_resource.one",
35+
tfjsonpath.New("identity_id"),
36+
tfjsonpath.New("state_id"),
37+
),
38+
},
39+
},
40+
},
41+
})
42+
}

0 commit comments

Comments
 (0)