opentofu/internal/tofu/test_context_test.go
namgyalangmo cb2e9119aa
Update copyright notice (#1232)
Signed-off-by: namgyalangmo <75657887+namgyalangmo@users.noreply.github.com>
2024-02-08 09:48:59 +00:00

599 lines
16 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package tofu
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
ctymsgpack "github.com/zclconf/go-cty/cty/msgpack"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/lang/marks"
"github.com/opentofu/opentofu/internal/moduletest"
"github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/providers"
"github.com/opentofu/opentofu/internal/states"
"github.com/opentofu/opentofu/internal/tfdiags"
)
func TestTestContext_EvaluateAgainstState(t *testing.T) {
tcs := map[string]struct {
configs map[string]string
state *states.State
variables InputValues
provider *MockProvider
expectedDiags []tfdiags.Description
expectedStatus moduletest.Status
}{
"basic_passing": {
configs: map[string]string{
"main.tf": `
resource "test_resource" "a" {
value = "Hello, world!"
}
`,
"main.tftest.hcl": `
run "test_case" {
assert {
condition = test_resource.a.value == "Hello, world!"
error_message = "invalid value"
}
}
`,
},
state: states.BuildState(func(state *states.SyncState) {
state.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "a",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: encodeCtyValue(t, cty.ObjectVal(map[string]cty.Value{
"value": cty.StringVal("Hello, world!"),
})),
},
addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("test"),
})
}),
provider: &MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_resource": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {
Type: cty.String,
Required: true,
},
},
},
},
},
},
},
expectedStatus: moduletest.Pass,
},
"basic_passing_with_sensitive_value": {
configs: map[string]string{
"main.tf": `
resource "test_resource" "a" {
sensitive_value = "Shhhhh!"
}
`,
"main.tftest.hcl": `
run "test_case" {
assert {
condition = test_resource.a.sensitive_value == "Shhhhh!"
error_message = "invalid value"
}
}
`,
},
state: states.BuildState(func(state *states.SyncState) {
state.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "a",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: encodeCtyValue(t, cty.ObjectVal(map[string]cty.Value{
"sensitive_value": cty.StringVal("Shhhhh!"),
})),
AttrSensitivePaths: []cty.PathValueMarks{
{
Path: cty.GetAttrPath("sensitive_value"),
Marks: cty.NewValueMarks(marks.Sensitive),
},
},
},
addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("test"),
})
}),
provider: &MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_resource": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"sensitive_value": {
Type: cty.String,
Required: true,
},
},
},
},
},
},
},
expectedStatus: moduletest.Pass,
},
"with_variables": {
configs: map[string]string{
"main.tf": `
variable "value" {
type = string
}
resource "test_resource" "a" {
value = var.value
}
`,
"main.tftest.hcl": `
variables {
value = "Hello, world!"
}
run "test_case" {
assert {
condition = test_resource.a.value == var.value
error_message = "invalid value"
}
}
`,
},
state: states.BuildState(func(state *states.SyncState) {
state.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "a",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: encodeCtyValue(t, cty.ObjectVal(map[string]cty.Value{
"value": cty.StringVal("Hello, world!"),
})),
},
addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("test"),
})
}),
variables: InputValues{
"value": {
Value: cty.StringVal("Hello, world!"),
},
},
provider: &MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_resource": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {
Type: cty.String,
Required: true,
},
},
},
},
},
},
},
expectedStatus: moduletest.Pass,
},
"basic_failing": {
configs: map[string]string{
"main.tf": `
resource "test_resource" "a" {
value = "Hello, world!"
}
`,
"main.tftest.hcl": `
run "test_case" {
assert {
condition = test_resource.a.value == "incorrect!"
error_message = "invalid value"
}
}
`,
},
state: states.BuildState(func(state *states.SyncState) {
state.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "a",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: encodeCtyValue(t, cty.ObjectVal(map[string]cty.Value{
"value": cty.StringVal("Hello, world!"),
})),
},
addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("test"),
})
}),
provider: &MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_resource": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {
Type: cty.String,
Required: true,
},
},
},
},
},
},
},
expectedStatus: moduletest.Fail,
expectedDiags: []tfdiags.Description{
{
Summary: "Test assertion failed",
Detail: "invalid value",
},
},
},
"two_failing_assertions": {
configs: map[string]string{
"main.tf": `
resource "test_resource" "a" {
value = "Hello, world!"
}
`,
"main.tftest.hcl": `
run "test_case" {
assert {
condition = test_resource.a.value == "incorrect!"
error_message = "invalid value"
}
assert {
condition = test_resource.a.value == "also incorrect!"
error_message = "still invalid"
}
}
`,
},
state: states.BuildState(func(state *states.SyncState) {
state.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "a",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: encodeCtyValue(t, cty.ObjectVal(map[string]cty.Value{
"value": cty.StringVal("Hello, world!"),
})),
},
addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("test"),
})
}),
provider: &MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_resource": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {
Type: cty.String,
Required: true,
},
},
},
},
},
},
},
expectedStatus: moduletest.Fail,
expectedDiags: []tfdiags.Description{
{
Summary: "Test assertion failed",
Detail: "invalid value",
},
{
Summary: "Test assertion failed",
Detail: "still invalid",
},
},
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
config := testModuleInline(t, tc.configs)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(tc.provider),
},
})
run := moduletest.Run{
Config: config.Module.Tests["main.tftest.hcl"].Runs[0],
Name: "test_case",
}
tctx := ctx.TestContext(config, tc.state, &plans.Plan{}, tc.variables)
tctx.EvaluateAgainstState(&run)
if expected, actual := tc.expectedStatus, run.Status; expected != actual {
t.Errorf("expected status \"%s\" but got \"%s\"", expected, actual)
}
compareDiagnosticsFromTestResult(t, tc.expectedDiags, run.Diagnostics)
})
}
}
func TestTestContext_EvaluateAgainstPlan(t *testing.T) {
tcs := map[string]struct {
configs map[string]string
state *states.State
plan *plans.Plan
variables InputValues
provider *MockProvider
expectedDiags []tfdiags.Description
expectedStatus moduletest.Status
}{
"basic_passing": {
configs: map[string]string{
"main.tf": `
resource "test_resource" "a" {
value = "Hello, world!"
}
`,
"main.tftest.hcl": `
run "test_case" {
assert {
condition = test_resource.a.value == "Hello, world!"
error_message = "invalid value"
}
}
`,
},
state: states.BuildState(func(state *states.SyncState) {
state.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "a",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectPlanned,
AttrsJSON: encodeCtyValue(t, cty.NullVal(cty.Object(map[string]cty.Type{
"value": cty.String,
}))),
},
addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("test"),
})
}),
plan: &plans.Plan{
Changes: &plans.Changes{
Resources: []*plans.ResourceInstanceChangeSrc{
{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "a",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("test"),
},
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: nil,
After: encodeDynamicValue(t, cty.ObjectVal(map[string]cty.Value{
"value": cty.StringVal("Hello, world!"),
})),
},
},
},
},
},
provider: &MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_resource": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {
Type: cty.String,
Required: true,
},
},
},
},
},
},
},
expectedStatus: moduletest.Pass,
},
"basic_failing": {
configs: map[string]string{
"main.tf": `
resource "test_resource" "a" {
value = "Hello, world!"
}
`,
"main.tftest.hcl": `
run "test_case" {
assert {
condition = test_resource.a.value == "incorrect!"
error_message = "invalid value"
}
}
`,
},
state: states.BuildState(func(state *states.SyncState) {
state.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "a",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectPlanned,
AttrsJSON: encodeCtyValue(t, cty.NullVal(cty.Object(map[string]cty.Type{
"value": cty.String,
}))),
},
addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("test"),
})
}),
plan: &plans.Plan{
Changes: &plans.Changes{
Resources: []*plans.ResourceInstanceChangeSrc{
{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "a",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("test"),
},
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: nil,
After: encodeDynamicValue(t, cty.ObjectVal(map[string]cty.Value{
"value": cty.StringVal("Hello, world!"),
})),
},
},
},
},
},
provider: &MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_resource": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {
Type: cty.String,
Required: true,
},
},
},
},
},
},
},
expectedStatus: moduletest.Fail,
expectedDiags: []tfdiags.Description{
{
Summary: "Test assertion failed",
Detail: "invalid value",
},
},
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
config := testModuleInline(t, tc.configs)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(tc.provider),
},
})
run := moduletest.Run{
Config: config.Module.Tests["main.tftest.hcl"].Runs[0],
Name: "test_case",
}
tctx := ctx.TestContext(config, tc.state, tc.plan, tc.variables)
tctx.EvaluateAgainstPlan(&run)
if expected, actual := tc.expectedStatus, run.Status; expected != actual {
t.Errorf("expected status \"%s\" but got \"%s\"", expected, actual)
}
compareDiagnosticsFromTestResult(t, tc.expectedDiags, run.Diagnostics)
})
}
}
func compareDiagnosticsFromTestResult(t *testing.T, expected []tfdiags.Description, actual tfdiags.Diagnostics) {
if len(expected) != len(actual) {
t.Errorf("found invalid number of diagnostics, expected %d but found %d", len(expected), len(actual))
}
length := len(expected)
if len(actual) > length {
length = len(actual)
}
for ix := 0; ix < length; ix++ {
if ix >= len(expected) {
t.Errorf("found extra diagnostic at %d:\n%v", ix, actual[ix].Description())
} else if ix >= len(actual) {
t.Errorf("missing diagnostic at %d:\n%v", ix, expected[ix])
} else {
expected := expected[ix]
actual := actual[ix].Description()
if diff := cmp.Diff(expected, actual); len(diff) > 0 {
t.Errorf("found different diagnostics at %d:\nexpected:\n%s\nactual:\n%s\ndiff:%s", ix, expected, actual, diff)
}
}
}
}
func encodeDynamicValue(t *testing.T, value cty.Value) []byte {
data, err := ctymsgpack.Marshal(value, value.Type())
if err != nil {
t.Fatalf("failed to marshal JSON: %s", err)
}
return data
}
func encodeCtyValue(t *testing.T, value cty.Value) []byte {
data, err := ctyjson.Marshal(value, value.Type())
if err != nil {
t.Fatalf("failed to marshal JSON: %s", err)
}
return data
}