diff --git a/check/key.go b/check/key.go new file mode 100644 index 0000000..330c30d --- /dev/null +++ b/check/key.go @@ -0,0 +1,154 @@ +package check + +import ( + "encoding/json" + "fmt" + "reflect" + + "github.com/Azure/terratest-terraform-fluent/testerror" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/stretchr/testify/assert" +) + +// JsonAssertionFunc is a function which can be used to unmarshal a raw JSON message and check its contents. +type JsonAssertionFunc func(input json.RawMessage) (*bool, error) + +// ThatTypeWithKey is a type which can be used for more fluent assertions for a given Resource & Key combination +type ThatTypeWithKey struct { + Plan *terraform.PlanStruct + ResourceName string + Key string +} + +// HasValue returns a *testerror.Error if the resource does not exist in the plan or if the value of the key does not match the +// expected value +func (twk ThatTypeWithKey) HasValue(expected interface{}) *testerror.Error { + if err := twk.Exists(); err != nil { + return err + } + + resource := twk.Plan.ResourcePlannedValuesMap[twk.ResourceName] + actual := resource.AttributeValues[twk.Key] + + if err := validateEqualArgs(expected, actual); err != nil { + return testerror.Newf("invalid operation: %#v == %#v (%s)", + expected, + actual, + err, + ) + } + + if !assert.ObjectsAreEqualValues(actual, expected) { + return testerror.Newf( + "%s: attribute %s, planned value %s not equal to assertion %s", + twk.ResourceName, + twk.Key, + actual, + expected, + ) + } + return nil +} + +// ContainsJsonValue returns a *testerror.Error which asserts upon a given JSON string set into +// the State by deserializing it and then asserting on it via the JsonAssertionFunc. +func (twk ThatTypeWithKey) ContainsJsonValue(assertion JsonAssertionFunc) *testerror.Error { + if err := twk.Exists(); err != nil { + return err + } + + if twk.HasValue("") == nil { + return testerror.Newf( + "%s: key %s was empty", + twk.ResourceName, + twk.Key, + ) + } + + resource := twk.Plan.ResourcePlannedValuesMap[twk.ResourceName] + actual := resource.AttributeValues[twk.Key] + j := json.RawMessage(actual.(string)) + ok, err := assertion(j) + if err != nil { + return testerror.Newf( + "%s: asserting value for %q: %+v", + twk.ResourceName, + twk.Key, + err, + ) + } + + if ok == nil || !*ok { + return testerror.Newf( + "%s: assertion failed for %q: %+v", + twk.ResourceName, + twk.Key, + err, + ) + } + + return nil +} + +// Exists returns a *testerror.Error if the resource does not exist in the plan or if the key does not exist in the resource +func (twk ThatTypeWithKey) Exists() *testerror.Error { + if err := InPlan(twk.Plan).That(twk.ResourceName).Exists(); err != nil { + return testerror.New(err.Error()) + } + + resource := twk.Plan.ResourcePlannedValuesMap[twk.ResourceName] + if _, exists := resource.AttributeValues[twk.Key]; !exists { + return testerror.Newf( + "%s: key %s not found in resource", + twk.ResourceName, + twk.Key, + ) + } + return nil +} + +// DoesNotExist returns a *testerror.Error if the resource does not exist in the plan or if the key exists in the resource +func (twk ThatTypeWithKey) DoesNotExist() *testerror.Error { + if err := InPlan(twk.Plan).That(twk.ResourceName).Exists(); err != nil { + return testerror.Newf(err.Error()) + } + + resource := twk.Plan.ResourcePlannedValuesMap[twk.ResourceName] + if _, exists := resource.AttributeValues[twk.Key]; exists { + return testerror.Newf( + "%s: key %s found in resource", + twk.ResourceName, + twk.Key, + ) + } + return nil +} + +func (twk ThatTypeWithKey) Query(q string) ThatTypeWithKeyQuery { + return ThatTypeWithKeyQuery{ + Plan: twk.Plan, + ResourceName: twk.ResourceName, + Key: twk.Key, + Query: q, + } +} + +// validateEqualArgs checks whether provided arguments can be safely used in the +// HasValue function. +func validateEqualArgs(expected, actual interface{}) error { + if expected == nil && actual == nil { + return nil + } + + if isFunction(expected) || isFunction(actual) { + return fmt.Errorf("cannot take func type as argument") + } + return nil +} + +func isFunction(arg interface{}) bool { + if arg == nil { + return false + } + return reflect.TypeOf(arg).Kind() == reflect.Func +} diff --git a/check/key_test.go b/check/key_test.go new file mode 100644 index 0000000..d12aa55 --- /dev/null +++ b/check/key_test.go @@ -0,0 +1,254 @@ +package check + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "testing" + + "github.com/Azure/terratest-terraform-fluent/setuptest" + "github.com/Azure/terratest-terraform-fluent/to" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHasValueInvalidArgs(t *testing.T) { + t.Parallel() + + tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + InPlan(tftest.Plan).That("local_file.test").Key("content").HasValue(func() {}).ErrorContains(t, "invalid operation") +} + +func TestHasValueStrings(t *testing.T) { + t.Parallel() + + tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + InPlan(tftest.Plan).That("local_file.test").Key("content").HasValue("test").ErrorIsNil(t) +} + +func TestHasValueStringsNotEqualError(t *testing.T) { + t.Parallel() + + tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + assert.ErrorContains( + t, + InPlan(tftest.Plan).That("local_file.test").Key("content").HasValue("throwError"), + "attribute content, planned value test not equal to assertion throwError", + ) +} + +func TestHasValueStringsToInt(t *testing.T) { + t.Parallel() + + tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + assert.Error( + t, + InPlan(tftest.Plan).That("local_file.test_int").Key("content").HasValue(123).AsError(), + ) +} + +func TestKeyNotExistsError(t *testing.T) { + t.Parallel() + + tftest, _ := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) + defer tftest.Cleanup() + assert.ErrorContains( + t, + InPlan(tftest.Plan).That("local_file.test").Key("not_exists").Exists(), + "key not_exists not found in resource", + ) +} + +func TestKeyNotExists(t *testing.T) { + t.Parallel() + + tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) + defer tftest.Cleanup() + require.NoError(t, err) + InPlan(tftest.Plan).That("local_file.test").Key("not_exists").DoesNotExist().ErrorIsNil(t) +} + +func TestKeyNotExistsFail(t *testing.T) { + t.Parallel() + + tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) + defer tftest.Cleanup() + require.NoError(t, err) + require.Errorf(t, InPlan(tftest.Plan).That("local_file.test").Key("content").DoesNotExist(), "key content not found in resource when it should be") +} + +func TestInSubdir(t *testing.T) { + t.Parallel() + + tftest, err := setuptest.Dirs("testdata/test-in-subdir", "subdir").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + InPlan(tftest.Plan).That("module.test.local_file.test").Key("content").HasValue("test").ErrorIsNil(t) +} + +func TestInSubdirFail(t *testing.T) { + t.Parallel() + + _, err := setuptest.Dirs("testdata/test-in-subdir", "not_exist").WithVars(nil).InitPlanShow(t) + require.True(t, os.IsNotExist(err)) +} + +func TestJsonArrayAssertionFunc(t *testing.T) { + t.Parallel() + + f := func(input json.RawMessage) (*bool, error) { + i := make([]interface{}, 0, 1) + if err := json.Unmarshal(input, &i); err != nil { + return nil, fmt.Errorf("JSON input is not an array") + } + if len(i) == 0 { + return nil, fmt.Errorf("JSON input is empty") + } + if i[0].(map[string]interface{})["test"] != "test" { + return nil, fmt.Errorf("JSON input key name is not equal to test") + } + + return to.Ptr(true), nil + } + + tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + InPlan(tftest.Plan).That("local_file.test_array_json").Key("content").ContainsJsonValue(JsonAssertionFunc(f)).ErrorIsNil(t) +} + +func TestJsonEmpty(t *testing.T) { + t.Parallel() + + f := JsonAssertionFunc( + func(input json.RawMessage) (*bool, error) { + return to.Ptr(true), nil + }, + ) + + tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + InPlan(tftest.Plan).That("local_file.test_empty_json").Key("content").ContainsJsonValue(f).ErrorContains(t, "key content was empty") +} + +func TestJsonAssertionFuncError(t *testing.T) { + t.Parallel() + + f := JsonAssertionFunc( + func(input json.RawMessage) (*bool, error) { + return to.Ptr(true), errors.New("test error") + }, + ) + + tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + InPlan(tftest.Plan).That("local_file.test_simple_json").Key("content").ContainsJsonValue(f).ErrorContains(t, "test error") +} + +func TestJsonAssertionFuncFalse(t *testing.T) { + t.Parallel() + + f := JsonAssertionFunc( + func(input json.RawMessage) (*bool, error) { + return to.Ptr(false), nil + }, + ) + + tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + InPlan(tftest.Plan).That("local_file.test_simple_json").Key("content").ContainsJsonValue(f).ErrorContains(t, "assertion failed for \"content\"") +} + +func TestJsonAssertionFuncNil(t *testing.T) { + t.Parallel() + + f := JsonAssertionFunc( + func(input json.RawMessage) (*bool, error) { + return nil, nil + }, + ) + + tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + InPlan(tftest.Plan).That("local_file.test_simple_json").Key("content").ContainsJsonValue(f).ErrorContains(t, "assertion failed for \"content\"") +} + +func TestJsonSimpleAssertionFunc(t *testing.T) { + t.Parallel() + + f := JsonAssertionFunc( + func(input json.RawMessage) (*bool, error) { + i := make(map[string]interface{}) + if err := json.Unmarshal(input, &i); err != nil { + return nil, fmt.Errorf("JSON input is not an map") + } + if len(i) == 0 { + return nil, fmt.Errorf("JSON input is empty") + } + if i["test"] != "test" { + return to.Ptr(false), nil + } + return to.Ptr(true), nil + }, + ) + + tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + InPlan(tftest.Plan).That("local_file.test_simple_json").Key("content").ContainsJsonValue(f).ErrorIsNil(t) +} + +func TestKeyDoesNotExist(t *testing.T) { + t.Parallel() + + tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + InPlan(tftest.Plan).That("local_file.test").Key("not_exist").DoesNotExist().ErrorIsNil(t) +} + +func TestKeyDoesNotExistFail(t *testing.T) { + t.Parallel() + + tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + err = InPlan(tftest.Plan).That("local_file.test").Key("content").DoesNotExist() + require.ErrorContains(t, err, "local_file.test: key content found in resource") +} + +func TestValidateEqualArgs(t *testing.T) { + require.Nil(t, validateEqualArgs(nil, nil)) +} + +func TestValidateEqualArgsFuncFail(t *testing.T) { + f1 := func() {} + f2 := func() {} + assert.ErrorContains(t, validateEqualArgs(f1, nil), "cannot take func type as argument") + assert.ErrorContains(t, validateEqualArgs(nil, f2), "cannot take func type as argument") +} + +func TestIsFunction(t *testing.T) { + f := func() {} + assert.True(t, isFunction(f)) +} + +func TestIsFunctionNot(t *testing.T) { + i := 1 + assert.False(t, isFunction(i)) + var s *string + assert.False(t, isFunction(s)) +} diff --git a/check/query.go b/check/query.go new file mode 100644 index 0000000..b8cce7c --- /dev/null +++ b/check/query.go @@ -0,0 +1,59 @@ +package check + +import ( + "encoding/json" + + "github.com/Azure/terratest-terraform-fluent/testerror" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" +) + +// ThatTypeWithKey is a type which can be used for more fluent assertions for a given Resource & Key combination, +// together with a gjson query https://github.com/tidwall/gjson +type ThatTypeWithKeyQuery struct { + Plan *terraform.PlanStruct + ResourceName string + Key string + Query string +} + +// HasValue executes the provided gjson query on the resource and key combination +// and tests the result against the provided value. +// https://github.com/tidwall/gjson +func (twkq ThatTypeWithKeyQuery) HasValue(expected interface{}) *testerror.Error { + + err := ThatTypeWithKey{ + Plan: twkq.Plan, + ResourceName: twkq.ResourceName, + Key: twkq.Key, + }.Exists() + + if err != nil { + return err + } + + resource := twkq.Plan.ResourcePlannedValuesMap[twkq.ResourceName] + actual := resource.AttributeValues[twkq.Key] + bytes, _ := json.Marshal(actual) + result := gjson.GetBytes(bytes, twkq.Query) + + if err := validateEqualArgs(expected, result.Value()); err != nil { + return testerror.Newf("invalid operation: %#v == %#v (%s)", + expected, + actual, + err, + ) + } + + if !assert.ObjectsAreEqualValues(result.Value(), expected) { + return testerror.Newf( + "%s: query result %v, for key %s not equal to assertion %v", + twkq.ResourceName, + result.Value(), + twkq.Key, + expected, + ) + } + return nil +} diff --git a/check/query_test.go b/check/query_test.go new file mode 100644 index 0000000..964d2e9 --- /dev/null +++ b/check/query_test.go @@ -0,0 +1,97 @@ +package check + +import ( + "testing" + + "github.com/Azure/terratest-terraform-fluent/setuptest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + queryTestData = "testdata/query" +) + +func TestQueryMap(t *testing.T) { + t.Parallel() + + tftest, err := setuptest.Dirs(queryTestData, "").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + assert.NoError( + t, + InPlan(tftest.Plan).That("terraform_data.test_map").Key("input").Query("test_key").HasValue("test").AsError(), + ) +} + +func TestQueryMapLength(t *testing.T) { + t.Parallel() + + tftest, err := setuptest.Dirs(queryTestData, "").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + assert.NoError( + t, + InPlan(tftest.Plan).That("terraform_data.test_map_list").Key("input").Query("test_key.#").HasValue(2).AsError(), + ) +} + +func TestQueryNestedMap(t *testing.T) { + t.Parallel() + + tftest, err := setuptest.Dirs(queryTestData, "").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + assert.NoError( + t, + InPlan(tftest.Plan).That("terraform_data.test_nested_map").Key("input").Query("test_key.nested_key").HasValue("test_nested").AsError(), + ) +} + +func TestQueryNil(t *testing.T) { + t.Parallel() + + tftest, err := setuptest.Dirs(queryTestData, "").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + assert.NoError( + t, + InPlan(tftest.Plan).That("terraform_data.invalid_json").Key("input").Query(".").HasValue(nil).AsError(), + ) +} + +func TestQueryInvalidArgs(t *testing.T) { + t.Parallel() + + tftest, err := setuptest.Dirs(queryTestData, "").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + + InPlan(tftest.Plan).That("terraform_data.invalid_json").Key("input").Query(".").HasValue(func() {}).ErrorContains(t, "invalid operation") +} + +func TestQueryNotEqual(t *testing.T) { + t.Parallel() + + tftest, err := setuptest.Dirs(queryTestData, "").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + assert.ErrorContains( + t, + InPlan(tftest.Plan).That("terraform_data.invalid_json").Key("input").Query(".").HasValue(123).AsError(), + "query result , for key input not equal to assertion 123", + ) +} + +func TestQueryNotExists(t *testing.T) { + t.Parallel() + + tftest, err := setuptest.Dirs(queryTestData, "").WithVars(nil).InitPlanShow(t) + require.NoError(t, err) + defer tftest.Cleanup() + assert.ErrorContains( + t, + InPlan(tftest.Plan).That("terraform_data.invalid_json").Key("not_exists").Query(".").HasValue(123).AsError(), + "key not_exists not found in resource", + ) +} diff --git a/check/testdata/query/main.tf b/check/testdata/query/main.tf new file mode 100644 index 0000000..dcaff0f --- /dev/null +++ b/check/testdata/query/main.tf @@ -0,0 +1,32 @@ +terraform { + required_version = ">= 1.4.0" +} + +resource "terraform_data" "test_map" { + input = tomap({ + "test_key" = "test", + "test_key_2" = "test2" + }) +} + +resource "terraform_data" "test_map_list" { + input = tomap({ + "test_key" = ["test", "test2"], + "test_key_2" = ["testA", "testB"] + }) +} + +resource "terraform_data" "test_nested_map" { + input = tomap({ + "test_key" = tomap({ + "nested_key" = "test_nested" + }), + "test_key_2" = tomap({ + "nested_key2" = "test_nested2" + }), + }) +} + +resource "terraform_data" "invalid_json" { + input = null +} diff --git a/check/that.go b/check/that.go index b88cc40..57e6eec 100644 --- a/check/that.go +++ b/check/that.go @@ -1,13 +1,8 @@ package check import ( - "encoding/json" - "fmt" - "reflect" - "github.com/Azure/terratest-terraform-fluent/testerror" "github.com/gruntwork-io/terratest/modules/terraform" - "github.com/stretchr/testify/assert" ) // ThatType is a type which can be used for more fluent assertions for a given Resource @@ -16,9 +11,6 @@ type ThatType struct { ResourceName string } -// JsonAssertionFunc is a function which can be used to unmarshal a raw JSON message and check its contents. -type JsonAssertionFunc func(input json.RawMessage) (*bool, error) - // That returns a type which can be used for more fluent assertions for a given resource. func (p PlanType) That(resourceName string) ThatType { return ThatType{ @@ -57,134 +49,3 @@ func (t ThatType) Key(key string) ThatTypeWithKey { Key: key, } } - -// ThatTypeWithKey is a type which can be used for more fluent assertions for a given Resource & Key combination -type ThatTypeWithKey struct { - Plan *terraform.PlanStruct - ResourceName string - Key string -} - -// HasValue returns a *testerror.Error if the resource does not exist in the plan or if the value of the key does not match the -// expected value -func (twk ThatTypeWithKey) HasValue(expected interface{}) *testerror.Error { - if err := twk.Exists(); err != nil { - return err - } - - resource := twk.Plan.ResourcePlannedValuesMap[twk.ResourceName] - actual := resource.AttributeValues[twk.Key] - - if err := validateEqualArgs(expected, actual); err != nil { - return testerror.Newf("invalid operation: %#v == %#v (%s)", - expected, - actual, - err, - ) - } - - if !assert.ObjectsAreEqualValues(actual, expected) { - return testerror.Newf( - "%s: attribute %s, planned value %s not equal to assertion %s", - twk.ResourceName, - twk.Key, - actual, - expected, - ) - } - return nil -} - -// ContainsJsonValue returns a *testerror.Error which asserts upon a given JSON string set into -// the State by deserializing it and then asserting on it via the JsonAssertionFunc. -func (twk ThatTypeWithKey) ContainsJsonValue(assertion JsonAssertionFunc) *testerror.Error { - if err := twk.Exists(); err != nil { - return err - } - - if twk.HasValue("") == nil { - return testerror.Newf( - "%s: key %s was empty", - twk.ResourceName, - twk.Key, - ) - } - - resource := twk.Plan.ResourcePlannedValuesMap[twk.ResourceName] - actual := resource.AttributeValues[twk.Key] - j := json.RawMessage(actual.(string)) - ok, err := assertion(j) - if err != nil { - return testerror.Newf( - "%s: asserting value for %q: %+v", - twk.ResourceName, - twk.Key, - err, - ) - } - - if ok == nil || !*ok { - return testerror.Newf( - "%s: assertion failed for %q: %+v", - twk.ResourceName, - twk.Key, - err, - ) - } - - return nil -} - -// Exists returns a *testerror.Error if the resource does not exist in the plan or if the key does not exist in the resource -func (twk ThatTypeWithKey) Exists() *testerror.Error { - if err := InPlan(twk.Plan).That(twk.ResourceName).Exists(); err != nil { - return testerror.New(err.Error()) - } - - resource := twk.Plan.ResourcePlannedValuesMap[twk.ResourceName] - if _, exists := resource.AttributeValues[twk.Key]; !exists { - return testerror.Newf( - "%s: key %s not found in resource", - twk.ResourceName, - twk.Key, - ) - } - return nil -} - -// DoesNotExist returns a *testerror.Error if the resource does not exist in the plan or if the key exists in the resource -func (twk ThatTypeWithKey) DoesNotExist() *testerror.Error { - if err := InPlan(twk.Plan).That(twk.ResourceName).Exists(); err != nil { - return testerror.Newf(err.Error()) - } - - resource := twk.Plan.ResourcePlannedValuesMap[twk.ResourceName] - if _, exists := resource.AttributeValues[twk.Key]; exists { - return testerror.Newf( - "%s: key %s found in resource", - twk.ResourceName, - twk.Key, - ) - } - return nil -} - -// validateEqualArgs checks whether provided arguments can be safely used in the -// HasValue function. -func validateEqualArgs(expected, actual interface{}) error { - if expected == nil && actual == nil { - return nil - } - - if isFunction(expected) || isFunction(actual) { - return fmt.Errorf("cannot take func type as argument") - } - return nil -} - -func isFunction(arg interface{}) bool { - if arg == nil { - return false - } - return reflect.TypeOf(arg).Kind() == reflect.Func -} diff --git a/check/that_test.go b/check/that_test.go index c9a0a42..d28c530 100644 --- a/check/that_test.go +++ b/check/that_test.go @@ -1,10 +1,6 @@ package check import ( - "encoding/json" - "errors" - "fmt" - "os" "testing" "github.com/Azure/terratest-terraform-fluent/setuptest" @@ -16,66 +12,6 @@ const ( basicTestData = "testdata/basic" ) -func Bool(b bool) *bool { - var b2 = b - return &b2 -} - -func TestHasValueInvalidArgs(t *testing.T) { - t.Parallel() - - tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) - require.NoError(t, err) - defer tftest.Cleanup() - InPlan(tftest.Plan).That("local_file.test").Key("content").HasValue(func() {}).ErrorContains(t, "invalid operation") -} - -func TestHasValueStrings(t *testing.T) { - t.Parallel() - - tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) - require.NoError(t, err) - defer tftest.Cleanup() - InPlan(tftest.Plan).That("local_file.test").Key("content").HasValue("test").ErrorIsNil(t) -} - -func TestHasValueStringsNotEqualError(t *testing.T) { - t.Parallel() - - tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) - require.NoError(t, err) - defer tftest.Cleanup() - assert.ErrorContains( - t, - InPlan(tftest.Plan).That("local_file.test").Key("content").HasValue("throwError"), - "attribute content, planned value test not equal to assertion throwError", - ) -} - -func TestHasValueStringsToInt(t *testing.T) { - t.Parallel() - - tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) - require.NoError(t, err) - defer tftest.Cleanup() - assert.Error( - t, - InPlan(tftest.Plan).That("local_file.test_int").Key("content").HasValue(123).AsError(), - ) -} - -func TestKeyNotExistsError(t *testing.T) { - t.Parallel() - - tftest, _ := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) - defer tftest.Cleanup() - assert.ErrorContains( - t, - InPlan(tftest.Plan).That("local_file.test").Key("not_exists").Exists(), - "key not_exists not found in resource", - ) -} - func TestResourceExists(t *testing.T) { t.Parallel() @@ -100,149 +36,6 @@ func TestResourceExistsFail(t *testing.T) { ) } -func TestKeyNotExists(t *testing.T) { - t.Parallel() - - tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) - defer tftest.Cleanup() - require.NoError(t, err) - InPlan(tftest.Plan).That("local_file.test").Key("not_exists").DoesNotExist().ErrorIsNil(t) -} - -func TestKeyNotExistsFail(t *testing.T) { - t.Parallel() - - tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) - defer tftest.Cleanup() - require.NoError(t, err) - require.Errorf(t, InPlan(tftest.Plan).That("local_file.test").Key("content").DoesNotExist(), "key content not found in resource when it should be") -} - -func TestInSubdir(t *testing.T) { - t.Parallel() - - tftest, err := setuptest.Dirs("testdata/test-in-subdir", "subdir").WithVars(nil).InitPlanShow(t) - require.NoError(t, err) - defer tftest.Cleanup() - InPlan(tftest.Plan).That("module.test.local_file.test").Key("content").HasValue("test").ErrorIsNil(t) -} - -func TestInSubdirFail(t *testing.T) { - t.Parallel() - - _, err := setuptest.Dirs("testdata/test-in-subdir", "not_exist").WithVars(nil).InitPlanShow(t) - require.True(t, os.IsNotExist(err)) -} - -func TestJsonArrayAssertionFunc(t *testing.T) { - t.Parallel() - - f := func(input json.RawMessage) (*bool, error) { - i := make([]interface{}, 0, 1) - if err := json.Unmarshal(input, &i); err != nil { - return nil, fmt.Errorf("JSON input is not an array") - } - if len(i) == 0 { - return nil, fmt.Errorf("JSON input is empty") - } - if i[0].(map[string]interface{})["test"] != "test" { - return nil, fmt.Errorf("JSON input key name is not equal to test") - } - - return Bool(true), nil - } - - tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) - require.NoError(t, err) - defer tftest.Cleanup() - InPlan(tftest.Plan).That("local_file.test_array_json").Key("content").ContainsJsonValue(JsonAssertionFunc(f)).ErrorIsNil(t) -} - -func TestJsonEmpty(t *testing.T) { - t.Parallel() - - f := JsonAssertionFunc( - func(input json.RawMessage) (*bool, error) { - return Bool(false), nil - }, - ) - - tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) - require.NoError(t, err) - defer tftest.Cleanup() - InPlan(tftest.Plan).That("local_file.test_empty_json").Key("content").ContainsJsonValue(f).ErrorContains(t, "key content was empty") -} - -func TestJsonAssertionFuncError(t *testing.T) { - t.Parallel() - - f := JsonAssertionFunc( - func(input json.RawMessage) (*bool, error) { - return Bool(false), errors.New("test error") - }, - ) - - tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) - require.NoError(t, err) - defer tftest.Cleanup() - InPlan(tftest.Plan).That("local_file.test_simple_json").Key("content").ContainsJsonValue(f).ErrorContains(t, "test error") -} - -func TestJsonAssertionFuncFalse(t *testing.T) { - t.Parallel() - - f := JsonAssertionFunc( - func(input json.RawMessage) (*bool, error) { - return Bool(false), nil - }, - ) - - tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) - require.NoError(t, err) - defer tftest.Cleanup() - InPlan(tftest.Plan).That("local_file.test_simple_json").Key("content").ContainsJsonValue(f).ErrorContains(t, "assertion failed for \"content\"") -} - -func TestJsonAssertionFuncNil(t *testing.T) { - t.Parallel() - - f := JsonAssertionFunc( - func(input json.RawMessage) (*bool, error) { - return nil, nil - }, - ) - - tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) - require.NoError(t, err) - defer tftest.Cleanup() - InPlan(tftest.Plan).That("local_file.test_simple_json").Key("content").ContainsJsonValue(f).ErrorContains(t, "assertion failed for \"content\"") -} - -func TestJsonSimpleAssertionFunc(t *testing.T) { - t.Parallel() - - f := JsonAssertionFunc( - func(input json.RawMessage) (*bool, error) { - i := make(map[string]interface{}) - if err := json.Unmarshal(input, &i); err != nil { - return nil, fmt.Errorf("JSON input is not an map") - } - if len(i) == 0 { - return nil, fmt.Errorf("JSON input is empty") - } - if i["test"] != "test" { - return Bool(false), nil - } - return Bool(true), nil - }, - ) - - tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) - require.NoError(t, err) - defer tftest.Cleanup() - InPlan(tftest.Plan).That("local_file.test_simple_json").Key("content").ContainsJsonValue(f).ErrorIsNil(t) -} - func TestResourceDoesNotExist(t *testing.T) { t.Parallel() @@ -261,45 +54,3 @@ func TestResourceDoesNotExistFail(t *testing.T) { err = InPlan(tftest.Plan).That("local_file.test").DoesNotExist() require.ErrorContains(t, err, "local_file.test: resource found in plan") } - -func TestKeyDoesNotExist(t *testing.T) { - t.Parallel() - - tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) - require.NoError(t, err) - defer tftest.Cleanup() - InPlan(tftest.Plan).That("local_file.test").Key("not_exist").DoesNotExist().ErrorIsNil(t) -} - -func TestKeyDoesNotExistFail(t *testing.T) { - t.Parallel() - - tftest, err := setuptest.Dirs(basicTestData, "").WithVars(nil).InitPlanShow(t) - require.NoError(t, err) - defer tftest.Cleanup() - err = InPlan(tftest.Plan).That("local_file.test").Key("content").DoesNotExist() - require.ErrorContains(t, err, "local_file.test: key content found in resource") -} - -func TestValidateEqualArgs(t *testing.T) { - require.Nil(t, validateEqualArgs(nil, nil)) -} - -func TestValidateEqualArgsFuncFail(t *testing.T) { - f1 := func() {} - f2 := func() {} - assert.ErrorContains(t, validateEqualArgs(f1, nil), "cannot take func type as argument") - assert.ErrorContains(t, validateEqualArgs(nil, f2), "cannot take func type as argument") -} - -func TestIsFunction(t *testing.T) { - f := func() {} - assert.True(t, isFunction(f)) -} - -func TestIsFunctionNot(t *testing.T) { - i := 1 - assert.False(t, isFunction(i)) - var s *string - assert.False(t, isFunction(s)) -} diff --git a/go.mod b/go.mod index 22b4131..8efbdbc 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.19 require ( github.com/gruntwork-io/terratest v0.41.18 github.com/stretchr/testify v1.8.2 + github.com/tidwall/gjson v1.14.4 gopkg.in/matryer/try.v1 v1.0.0-20150601225556-312d2599e12e ) @@ -68,6 +69,8 @@ require ( github.com/pquerna/otp v1.4.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/tmccombs/hcl2json v0.5.0 // indirect github.com/ulikunitz/xz v0.5.11 // indirect github.com/urfave/cli/v2 v2.25.1 // indirect diff --git a/go.sum b/go.sum index 1a3c94a..5500887 100644 --- a/go.sum +++ b/go.sum @@ -476,6 +476,12 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tmccombs/hcl2json v0.5.0 h1:cT2sXStOzKL06c8ZTf9vh+0N8GKGzV7+9RUaY5/iUP8= github.com/tmccombs/hcl2json v0.5.0/go.mod h1:B0ZpBthAKbQur6yZRKrtaqDmYLCvgnwHOBApE0faCpU= github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= diff --git a/setuptest/dir.go b/setuptest/dir.go index e4ea6e9..d8f7cbd 100644 --- a/setuptest/dir.go +++ b/setuptest/dir.go @@ -31,6 +31,8 @@ type Response struct { // The test directory is the directory containing the test code, // it should either be blank to test the code in the root, // or a relative path beneath the root directory. +// +// Before a Terraform command is run, the code in the root directory will be copied to a temporary directory. func Dirs(rootdir, testdir string) DirType { return DirType{ RootDir: rootdir, diff --git a/setuptest/doc.go b/setuptest/doc.go index f5188ad..56c676f 100644 --- a/setuptest/doc.go +++ b/setuptest/doc.go @@ -1,3 +1,5 @@ // Package setuptest is used to setup a test environment for testing Terraform code. -// It also allows you to run Terraform commands like init, plan, apply, and destroy. +// +// It will copy the supplied directories to a temporary location before running +// Terraform commands like init, plan, apply, and destroy. package setuptest diff --git a/to/ptr.go b/to/ptr.go new file mode 100644 index 0000000..e6883c3 --- /dev/null +++ b/to/ptr.go @@ -0,0 +1,6 @@ +package to + +// Ptr returns a pointer to the provided value. +func Ptr[T any](v T) *T { + return &v +} diff --git a/to/ptr_test.go b/to/ptr_test.go new file mode 100644 index 0000000..c9b461f --- /dev/null +++ b/to/ptr_test.go @@ -0,0 +1,16 @@ +package to + +import ( + "testing" +) + +func TestPtr(t *testing.T) { + b := true + pb := Ptr(b) + if pb == nil { + t.Fatal("unexpected nil conversion") + } + if *pb != b { + t.Fatalf("got %v, want %v", *pb, b) + } +}