Skip to content

Commit 6d8e01e

Browse files
committed
add ExpectIdentity state check
1 parent c76bfd5 commit 6d8e01e

File tree

3 files changed

+520
-0
lines changed

3 files changed

+520
-0
lines changed

statecheck/expect_identity.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package statecheck
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"maps"
10+
"slices"
11+
"sort"
12+
13+
tfjson "github.com/hashicorp/terraform-json"
14+
15+
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
16+
)
17+
18+
var _ StateCheck = expectIdentity{}
19+
20+
type expectIdentity struct {
21+
resourceAddress string
22+
identity map[string]knownvalue.Check
23+
}
24+
25+
// CheckState implements the state check logic.
26+
func (e expectIdentity) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) {
27+
var resource *tfjson.StateResource
28+
29+
if req.State == nil {
30+
resp.Error = fmt.Errorf("state is nil")
31+
32+
return
33+
}
34+
35+
if req.State.Values == nil {
36+
resp.Error = fmt.Errorf("state does not contain any state values")
37+
38+
return
39+
}
40+
41+
if req.State.Values.RootModule == nil {
42+
resp.Error = fmt.Errorf("state does not contain a root module")
43+
44+
return
45+
}
46+
47+
for _, r := range req.State.Values.RootModule.Resources {
48+
if e.resourceAddress == r.Address {
49+
resource = r
50+
51+
break
52+
}
53+
}
54+
55+
if resource == nil {
56+
resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddress)
57+
58+
return
59+
}
60+
61+
if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 {
62+
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)
63+
64+
return
65+
}
66+
67+
if len(resource.IdentityValues) != len(e.identity) {
68+
deltaMsg := ""
69+
if len(resource.IdentityValues) > len(e.identity) {
70+
deltaMsg = createDeltaString(resource.IdentityValues, e.identity, "actual identity has extra attribute(s): ")
71+
} else {
72+
deltaMsg = createDeltaString(e.identity, resource.IdentityValues, "actual identity is missing attribute(s): ")
73+
}
74+
75+
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)
76+
return
77+
}
78+
79+
var keys []string
80+
81+
for k := range e.identity {
82+
keys = append(keys, k)
83+
}
84+
85+
sort.SliceStable(keys, func(i, j int) bool {
86+
return keys[i] < keys[j]
87+
})
88+
89+
for _, k := range keys {
90+
actualIdentityVal, ok := resource.IdentityValues[k]
91+
92+
if !ok {
93+
resp.Error = fmt.Errorf("%s - missing attribute %q in actual identity object", e.resourceAddress, k)
94+
return
95+
}
96+
97+
if err := e.identity[k].CheckValue(actualIdentityVal); err != nil {
98+
resp.Error = fmt.Errorf("%s - %q identity attribute: %s", e.resourceAddress, k, err)
99+
return
100+
}
101+
}
102+
}
103+
104+
// ExpectIdentity returns a state check that asserts that the identity at the given resource matches a known object, where each
105+
// map key represents an identity attribute name. The identity in state must exactly match the given object and any missing/extra
106+
// attributes will raise a diagnostic.
107+
//
108+
// This state check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+
109+
func ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check) StateCheck {
110+
return expectIdentity{
111+
resourceAddress: resourceAddress,
112+
identity: identity,
113+
}
114+
}
115+
116+
// createDeltaString prints the map keys that are present in mapA and not present in mapB
117+
func createDeltaString[T any, V any](mapA map[string]T, mapB map[string]V, msgPrefix string) string {
118+
deltaMsg := ""
119+
120+
deltaMap := make(map[string]T, len(mapA))
121+
maps.Copy(deltaMap, mapA)
122+
for key := range mapB {
123+
delete(deltaMap, key)
124+
}
125+
126+
deltaKeys := slices.Sorted(maps.Keys(deltaMap))
127+
128+
for i, k := range deltaKeys {
129+
if i == 0 {
130+
deltaMsg += msgPrefix
131+
} else if i != 0 {
132+
deltaMsg += ", "
133+
}
134+
deltaMsg += fmt.Sprintf("%q", k)
135+
}
136+
137+
return deltaMsg
138+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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/knownvalue"
11+
"github.com/hashicorp/terraform-plugin-testing/statecheck"
12+
"github.com/hashicorp/terraform-plugin-testing/tfversion"
13+
)
14+
15+
func ExampleExpectIdentity() {
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" has an identity schema with "id" and "name" string attributes
26+
Steps: []resource.TestStep{
27+
{
28+
Config: `resource "test_resource" "one" {}`,
29+
ConfigStateChecks: []statecheck.StateCheck{
30+
statecheck.ExpectIdentity(
31+
"test_resource.one",
32+
map[string]knownvalue.Check{
33+
"id": knownvalue.StringExact("id-123"),
34+
"name": knownvalue.StringExact("John Doe"),
35+
},
36+
),
37+
},
38+
},
39+
},
40+
})
41+
}

0 commit comments

Comments
 (0)