Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding State Checks for Known Type and Value, and Sensitive Checks #275

Merged
merged 30 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f67b3e7
Adding StateCheck interface (#266)
bendbennett Jan 9, 2024
eed2a12
Adding validation to ensure state checks are only defined for config …
bendbennett Jan 9, 2024
d4e31d3
Adding ExpectKnownValue state check (#266)
bendbennett Jan 9, 2024
4e3ca3a
Adding ExpectKnownOutputValue state check (#266)
bendbennett Jan 9, 2024
d93aa82
Adding ExpectKnownOutputValueAtPath state check (#266)
bendbennett Jan 9, 2024
598dac8
Modifying ExpectKnown<Value|OutputValue|OutputValueAtPath> to allow f…
bendbennett Jan 10, 2024
c78e3e8
Adding ExpectSensitiveValue state check (#266)
bendbennett Jan 10, 2024
1e51693
Adding documentation for state checks and null known value check type…
bendbennett Jan 10, 2024
3d4acf4
Adding to the documentation for the custom known value check (#266)
bendbennett Jan 11, 2024
01fe9a9
Adding changelog entries (#266)
bendbennett Jan 11, 2024
fb9fed8
Refactoring to use updated known value check types (#266)
bendbennett Jan 11, 2024
7ff68bc
Correcting documentation for revised naming of known value check type…
bendbennett Jan 15, 2024
5d04859
Renaming nul known value check (#266)
bendbennett Jan 15, 2024
a79aea8
Fixing tests (#266)
bendbennett Jan 15, 2024
f5abf73
Adding address and path to state check errors (#266)
bendbennett Jan 15, 2024
648730a
Fixing navigation (#266)
bendbennett Jan 16, 2024
c74a9e8
Fixing changelog entries
bendbennett Jan 17, 2024
178c2c4
Modifying ExpectKnown<Value|OutputValue|OutputValueAtPath> to handle …
bendbennett Jan 18, 2024
c542a70
Deprecating ExpectNullOutputValue and ExpectNullOutputValueAtPath pla…
bendbennett Jan 18, 2024
518c94c
Adding return statements (#266)
bendbennett Jan 22, 2024
35ffc45
Adding change log entry for deprecation of `ExpectNullOutputValue` an…
bendbennett Jan 22, 2024
31f8d5e
Modifying return value of nullExact.String() (#266)
bendbennett Jan 22, 2024
031df21
Renaming variable (#266)
bendbennett Jan 22, 2024
6df33b9
Adding comment for Terraform v1.4.6 (#266)
bendbennett Jan 22, 2024
15330e3
Adding further tests for null exact known value type check (#266)
bendbennett Jan 22, 2024
e4e96ac
Merge branch 'main' into bendbennett/issues-266
bendbennett Jan 22, 2024
a88a10d
Linting (#266)
bendbennett Jan 22, 2024
6d8f112
Renaming BoolExact to Bool, and NullExact to Null (#266)
bendbennett Jan 23, 2024
04cf3c9
Removing ConfigStateChecks type (#266)
bendbennett Jan 23, 2024
173d4b2
Move execution of ConfigStateChecks (#266)
bendbennett Jan 23, 2024
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
6 changes: 6 additions & 0 deletions .changes/unreleased/FEATURES-20240111-142126.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: FEATURES
body: 'statecheck: Introduced new `statecheck` package with interface and built-in
state check functionality'
time: 2024-01-11T14:21:26.261094Z
custom:
Issue: "275"
6 changes: 6 additions & 0 deletions .changes/unreleased/FEATURES-20240111-142223.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: FEATURES
body: 'statecheck: Added `ExpectKnownValue` state check, which asserts that a given
resource attribute has a defined type, and value'
time: 2024-01-11T14:22:23.072321Z
custom:
Issue: "275"
6 changes: 6 additions & 0 deletions .changes/unreleased/FEATURES-20240111-142314.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: FEATURES
body: 'statecheck: Added `ExpectKnownOutputValue` state check, which asserts that
a given output value has a defined type, and value'
time: 2024-01-11T14:23:14.025585Z
custom:
Issue: "275"
6 changes: 6 additions & 0 deletions .changes/unreleased/FEATURES-20240111-142353.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: FEATURES
body: 'statecheck: Added `ExpectKnownOutputValueAtPath` plan check, which asserts
that a given output value at a specified path has a defined type, and value'
time: 2024-01-11T14:23:53.633255Z
custom:
Issue: "275"
6 changes: 6 additions & 0 deletions .changes/unreleased/FEATURES-20240111-142544.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: FEATURES
body: 'statecheck: Added `ExpectSensitiveValue` built-in state check, which asserts
that a given attribute has a sensitive value'
time: 2024-01-11T14:25:44.598583Z
custom:
Issue: "275"
7 changes: 7 additions & 0 deletions .changes/unreleased/NOTES-20240122-082628.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
kind: NOTES
body: 'plancheck: Deprecated `ExpectNullOutputValue` and `ExpectNullOutputValueAtPath`.
Use `ExpectKnownOutputValue` and `ExpectKnownOutputValueAtPath` with
`knownvalue.NullExact` instead'
time: 2024-01-22T08:26:28.053303Z
custom:
Issue: "275"
29 changes: 29 additions & 0 deletions helper/resource/state_checks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package resource

import (
"context"
"errors"

tfjson "github.com/hashicorp/terraform-json"
"github.com/mitchellh/go-testing-interface"

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

func runStateChecks(ctx context.Context, t testing.T, state *tfjson.State, stateChecks []statecheck.StateCheck) error {
t.Helper()

var result []error

for _, stateCheck := range stateChecks {
resp := statecheck.CheckStateResponse{}
stateCheck.CheckState(ctx, statecheck.CheckStateRequest{State: state}, &resp)

result = append(result, resp.Error)
}

return errors.Join(result...)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No more resource.ComposeAggregateTestCheckFunc vs resource.ComposeTestCheckFunc debates!

}
22 changes: 22 additions & 0 deletions helper/resource/state_checks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package resource

import (
"context"

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

var _ statecheck.StateCheck = &stateCheckSpy{}

type stateCheckSpy struct {
err error
called bool
}

func (s *stateCheckSpy) CheckState(ctx context.Context, req statecheck.CheckStateRequest, resp *statecheck.CheckStateResponse) {
s.called = true
resp.Error = s.err
}
12 changes: 12 additions & 0 deletions helper/resource/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

"github.com/hashicorp/terraform-plugin-testing/config"
"github.com/hashicorp/terraform-plugin-testing/plancheck"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/hashicorp/terraform-plugin-testing/tfversion"

Expand Down Expand Up @@ -590,6 +591,13 @@ type TestStep struct {
// [plancheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck
RefreshPlanChecks RefreshPlanChecks

// ConfigStateChecks allow assertions to be made against the state file at different points of a Config (apply) test using a state check.
// Custom state checks can be created by implementing the [StateCheck] interface, or by using a StateCheck implementation from the provided [statecheck] package
//
// [StateCheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/statecheck#StateCheck
// [statecheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/statecheck
ConfigStateChecks ConfigStateChecks
bendbennett marked this conversation as resolved.
Show resolved Hide resolved

// PlanOnly can be set to only run `plan` with this configuration, and not
// actually apply it. This is useful for ensuring config changes result in
// no-op plans
Expand Down Expand Up @@ -795,6 +803,10 @@ type RefreshPlanChecks struct {
PostRefresh []plancheck.PlanCheck
}

// ConfigStateChecks runs all state checks in the slice. This occurs after the apply and refresh of a Config test are run.
// All errors by state checks in this slice are aggregated, reported, and will result in a test failure.
type ConfigStateChecks []statecheck.StateCheck
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open question: do we need this additional exported type? I'm guessing this was for a little bit of consistency with ConfigPlanChecks, however that is there because we need a structure to capture the various times plan checks could be ran. In my experience, typically you would introduce something like this if there were plans to attach methods to the type, but since there are not any here right now, I'm curious if there were future plans for that. Another option would also be to put this type in the statecheck package, so all the implementation details live in one place. I'm just wondering if we should treat this similar to how the plan checks do use []plancheck.PlanCheck directly once its at that "level". Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're assumption is correct, the exported ConfigStateChecks type was purely for consistency. I've removed the type and replaced with the usage of []statecheck.StateCheck throughout.


// ParallelTest performs an acceptance test on a resource, allowing concurrency
// with other ParallelTest. The number of concurrent tests is controlled by the
// "go test" command -parallel flag.
Expand Down
20 changes: 20 additions & 0 deletions helper/resource/testing_new_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,26 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint
}
}

// Run post-apply, post-refresh state checks
if len(step.ConfigStateChecks) > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Historically we have ran state checks immediately after apply and before the additional plan checks (e.g. line 181 area) -- do we want to also do that here for consistency/ease of migration? In reality, the timing of the checks will determine which errors developers might see first: whether it be unexpectedly non-empty plans after apply or whether their state assertions are incorrect.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved the execution of the ConfigStateChecks so that they run immediately after any Check (i.e., line 210).

var state *tfjson.State

err = runProviderCommand(ctx, t, func() error {
var err error
state, err = wd.State(ctx)
return err
}, wd, providers)

if err != nil {
return fmt.Errorf("Error retrieving post-apply, post-refresh state: %w", err)
}

err = runStateChecks(ctx, t, state, step.ConfigStateChecks)
if err != nil {
return fmt.Errorf("Post-apply refresh state check(s) failed:\n%w", err)
}
}

// check if plan is empty
if !planIsEmpty(plan, helper.TerraformVersion()) && !step.ExpectNonEmptyPlan {
var stdout string
Expand Down
124 changes: 124 additions & 0 deletions helper/resource/testing_new_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

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

"github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider"
"github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver"
"github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource"
Expand Down Expand Up @@ -717,3 +718,126 @@ func Test_ConfigPlanChecks_PostApplyPostRefresh_Errors(t *testing.T) {
},
})
}

func Test_ConfigStateChecks_Called(t *testing.T) {
t.Parallel()

spy1 := &stateCheckSpy{}
spy2 := &stateCheckSpy{}
UnitTest(t, TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories
},
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
"test": providerserver.NewProviderServer(testprovider.Provider{
Resources: map[string]testprovider.Resource{
"test_resource": {
CreateResponse: &resource.CreateResponse{
NewState: tftypes.NewValue(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"id": tftypes.String,
},
},
map[string]tftypes.Value{
"id": tftypes.NewValue(tftypes.String, "test"),
},
),
},
SchemaResponse: &resource.SchemaResponse{
Schema: &tfprotov6.Schema{
Block: &tfprotov6.SchemaBlock{
Attributes: []*tfprotov6.SchemaAttribute{
{
Name: "id",
Type: tftypes.String,
Computed: true,
},
},
},
},
},
},
},
}),
},
Steps: []TestStep{
{
Config: `resource "test_resource" "test" {}`,
ConfigStateChecks: ConfigStateChecks{
spy1,
spy2,
},
},
},
})

if !spy1.called {
t.Error("expected ConfigStateChecks spy1 to be called at least once")
}

if !spy2.called {
t.Error("expected ConfigStateChecks spy2 to be called at least once")
}
}

func Test_ConfigStateChecks_Errors(t *testing.T) {
t.Parallel()

spy1 := &stateCheckSpy{}
spy2 := &stateCheckSpy{
err: errors.New("spy2 check failed"),
}
spy3 := &stateCheckSpy{
err: errors.New("spy3 check failed"),
}
UnitTest(t, TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories
},
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
"test": providerserver.NewProviderServer(testprovider.Provider{
Resources: map[string]testprovider.Resource{
"test_resource": {
CreateResponse: &resource.CreateResponse{
NewState: tftypes.NewValue(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"id": tftypes.String,
},
},
map[string]tftypes.Value{
"id": tftypes.NewValue(tftypes.String, "test"),
},
),
},
SchemaResponse: &resource.SchemaResponse{
Schema: &tfprotov6.Schema{
Block: &tfprotov6.SchemaBlock{
Attributes: []*tfprotov6.SchemaAttribute{
{
Name: "id",
Type: tftypes.String,
Computed: true,
},
},
},
},
},
},
},
}),
},
Steps: []TestStep{
{
Config: `resource "test_resource" "test" {}`,
ConfigStateChecks: ConfigStateChecks{
spy1,
spy2,
spy3,
},
ExpectError: regexp.MustCompile(`.*?(spy2 check failed)\n.*?(spy3 check failed)`),
},
},
})
}
8 changes: 7 additions & 1 deletion helper/resource/teststep_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
// testStepValidateRequest contains data for the (TestStep).validate() method.
type testStepValidateRequest struct {
// StepConfiguration contains the TestStep configuration derived from
// TestStep.Config or TestStep.ConfigDirectory.
// TestStep.Config, TestStep.ConfigDirectory, or TestStep.ConfigFile.
StepConfiguration teststep.Config

// StepNumber is the index of the TestStep in the TestCase.Steps.
Expand Down Expand Up @@ -235,5 +235,11 @@ func (s TestStep) validate(ctx context.Context, req testStepValidateRequest) err
return err
}

if len(s.ConfigStateChecks) > 0 && req.StepConfiguration == nil {
err := fmt.Errorf("TestStep ConfigStateChecks must only be specified with Config, ConfigDirectory or ConfigFile")
logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err})
return err
}

return nil
}
10 changes: 10 additions & 0 deletions helper/resource/teststep_validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,16 @@ func TestTestStepValidate(t *testing.T) {
testStepValidateRequest: testStepValidateRequest{TestCaseHasProviders: true},
expectedError: errors.New("TestStep ConfigPlanChecks.PostApplyPostRefresh must only be specified with Config"),
},
"configstatechecks-not-config-mode": {
testStep: TestStep{
ConfigStateChecks: ConfigStateChecks{
&stateCheckSpy{},
},
RefreshState: true,
},
testStepValidateRequest: testStepValidateRequest{TestCaseHasProviders: true},
expectedError: errors.New("TestStep ConfigStateChecks must only be specified with Config"),
},
"refreshplanchecks-postrefresh-not-refresh-mode": {
testStep: TestStep{
RefreshPlanChecks: RefreshPlanChecks{
Expand Down
32 changes: 32 additions & 0 deletions knownvalue/null.go
austinvalle marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package knownvalue

import (
"fmt"
)

var _ Check = nullExact{}

type nullExact struct{}

// CheckValue determines whether the passed value is nil.
func (v nullExact) CheckValue(other any) error {
if other != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I vaguely remember checking an any/interface{} for nil and I believe it's not as straightforward as this for things like maps. (I don't know if the underlying tfjson implementation will expose this problem, so it may not be relevant)

This test will fail:
image

And delve shows it slightly different, but still should be considered null:
image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This article explains the problem a bit, may have to dip into some reflection if we need to cover nil cases like maps and slices.

https://vitaneri.com/posts/check-for-nil-interface-in-go

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for raising this.

I've examined the values returned when using the tfjson implementation.

With a schema that looks as follows:

func (e *exampleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			/* ... */
			"list_attribute": schema.ListAttribute{
				Optional:    true,
				/* ... */
			},
			"map_attribute": schema.MapAttribute{
				Optional:    true,
				/* ... */
			},
			"object_attribute": schema.ObjectAttribute{
				Optional: true,
				AttributeTypes: map[string]attr.Type{
					/* ... */
				},
			},
			"set_attribute": schema.SetAttribute{
				Optional:    true,
				/* ... */
			},
			"list_nested_attribute": schema.ListNestedAttribute{
				Optional: true,
				NestedObject: schema.NestedAttributeObject{
					Attributes: map[string]schema.Attribute{
						/* ... */
					},
				},
			},
			"map_nested_attribute": schema.MapNestedAttribute{
				Optional: true,
				NestedObject: schema.NestedAttributeObject{
					Attributes: map[string]schema.Attribute{
						/* ... */
					},
				},
			},
			"set_nested_attribute": schema.SetNestedAttribute{
				Optional: true,
				NestedObject: schema.NestedAttributeObject{
					Attributes: map[string]schema.Attribute{
						/* ... */
					},
				},
			},
			"single_nested_attribute": schema.SingleNestedAttribute{
				Optional: true,
				Attributes: map[string]schema.Attribute{
					/* ... */
				},
			},
		},
		Blocks: map[string]schema.Block{
			"list_nested_block": schema.ListNestedBlock{
				NestedObject: schema.NestedBlockObject{
					Attributes: map[string]schema.Attribute{
						/* ... */
					},
					Blocks: map[string]schema.Block{
						"list_nested_nested_block": schema.ListNestedBlock{
							NestedObject: schema.NestedBlockObject{
								Attributes: map[string]schema.Attribute{
									/* ... */
								},
							},
						},
					},
				},
			},

			"set_nested_block": schema.SetNestedBlock{
				NestedObject: schema.NestedBlockObject{
					Attributes: map[string]schema.Attribute{
						/* ... */
					},
					Blocks: map[string]schema.Block{
						"set_nested_nested_block": schema.SetNestedBlock{
							NestedObject: schema.NestedBlockObject{
								Attributes: map[string]schema.Attribute{
									/* ... */
								},
							},
						},
					},
				},
			},
			"single_nested_block": schema.SingleNestedBlock{
				Attributes: map[string]schema.Attribute{
					/* ... */
				},
			},
		},
	}
}

The plan output looks as follows:

{
  "format_version": "1.2",
  "terraform_version": "1.7.0",
  "planned_values": {
    "root_module": {
      "resources": [
        {
          "address": "example_resource.example",
          "mode": "managed",
          "type": "example_resource",
          "name": "example",
          "provider_name": "registry.terraform.io/bendbennett/playground",
          "schema_version": 0,
          "values": {
            "list_attribute": null,
            "list_nested_attribute": null,
            "list_nested_block": [],
            "map_attribute": null,
            "map_nested_attribute": null,
            "object_attribute": null,
            "set_attribute": null,
            "set_nested_attribute": null,
            "set_nested_block": [],
            "single_nested_attribute": null,
            "single_nested_block": null,
          },
          "sensitive_values": {
            "list_nested_block": [],
            "set_nested_block": []
          }
        }
      ]
    }
  },
  "resource_changes": [
    {
      "address": "example_resource.example",
      "mode": "managed",
      "type": "example_resource",
      "name": "example",
      "provider_name": "registry.terraform.io/bendbennett/playground",
      "change": {
        "actions": [
          "create"
        ],
        "before": null,
        "after": {
          "list_attribute": null,
          "list_nested_attribute": null,
          "list_nested_block": [],
          "map_attribute": null,
          "map_nested_attribute": null,
          "object_attribute": null,
          "set_attribute": null,
          "set_nested_attribute": null,
          "set_nested_block": [],
          "single_nested_attribute": null,
          "single_nested_block": null,
        },
        "after_unknown": {
          "id": true,
          "list_nested_block": [],
          "set_nested_block": []
        },
        "before_sensitive": false,
        "after_sensitive": {
          "list_nested_block": [],
          "set_nested_block": []
        }
      }
    }
  ],
  "configuration": {
    "provider_config": {
      "example": {
        "name": "example",
        "full_name": "registry.terraform.io/bendbennett/playground"
      },
      "playground": {
        "name": "playground",
        "full_name": "registry.terraform.io/bendbennett/playground"
      }
    },
    "root_module": {
      "resources": [
        {
          "address": "example_resource.example",
          "mode": "managed",
          "type": "example_resource",
          "name": "example",
          "provider_config_key": "example",
          "schema_version": 0
        }
      ]
    }
  },
  "timestamp": "2024-01-22T10:12:40Z",
  "errored": false
}

The state looks as follows:

{
  "version": 4,
  "terraform_version": "1.7.0",
  "serial": 1,
  "lineage": "2c8dadf4-11dd-22b9-c419-5d07aa808890",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "example_resource",
      "name": "example",
      "provider": "provider[\"registry.terraform.io/bendbennett/playground\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "bool_attribute": null,
            "float64_attribute": null,
            "id": "example-id",
            "int64_attribute": null,
            "list_attribute": null,
            "list_nested_attribute": null,
            "list_nested_block": [],
            "map_attribute": null,
            "map_nested_attribute": null,
            "number_attribute": null,
            "object_attribute": null,
            "set_attribute": null,
            "set_nested_attribute": null,
            "set_nested_block": [],
            "single_nested_attribute": null,
            "single_nested_block": null,
            "string_attribute": null
          },
          "sensitive_attributes": []
        }
      ]
    }
  ],
  "check_results": null
}

Debugging during test execution shows the following:

image

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added some additional test coverage to the following:

  • TestExpectKnownValue_CheckState_AttributeValueNull
  • TestExpectKnownOutputValueAtPath_CheckState_AttributeValueNull
  • TestExpectKnownValue_CheckPlan_AttributeValueNull
  • TestExpectKnownOutputValueAtPath_CheckPlan_AttributeValueNull
  • TestExpectKnownOutputValue_CheckPlan_AttributeValueNull

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Today I learned! I think we have historically avoided these sorts of issues by not performing (re-)assignment like that article shows. Hopefully we can rely on the static analysis tooling to catch the situation if for some reason it does get introduced and the unit testing does not catch it, because I'm a proponent of simpler == better where possible.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding the tests, good to know it shouldn't be a problem for us 🙂

I'm a proponent of simpler == better where possible.

Agreed 👍🏻

return fmt.Errorf("expected value nil for NullExact check, got: %T", other)
}

return nil
}

// String returns the string representation of null.
func (v nullExact) String() string {
return "null"
}

// NullExact returns a Check for asserting equality nil
// and the value passed to the CheckValue method.
func NullExact() nullExact {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bikeshed (I'm so sorry): Are there other checks that could be run against a null value? Maybe this can be just Null if there is only the possibility for the value being null or not? I guess you could also say the same for boolean checks, but all the other typed checks could have additional checks. 🤷

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Agree with the suggested naming change here. I've made the following changes:

  • NullExact => Null
  • nullExact => null
  • BoolExact => bool
  • boolExact => boolValue

The docs have been updated in accordance with these changes.

return nullExact{}
}
Loading
Loading