opentofu/internal/command/views/json_view_test.go
Martin Atkins 034e944070 Move plans/ to internal/plans/
This is part of a general effort to move all of Terraform's non-library
package surface under internal in order to reinforce that these are for
internal use within Terraform only.

If you were previously importing packages under this prefix into an
external codebase, you could pin to an earlier release tag as an interim
solution until you've make a plan to achieve the same functionality some
other way.
2021-05-17 14:09:07 -07:00

308 lines
8.6 KiB
Go

package views
import (
"encoding/json"
"fmt"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/internal/addrs"
viewsjson "github.com/hashicorp/terraform/internal/command/views/json"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/internal/tfdiags"
tfversion "github.com/hashicorp/terraform/version"
)
// Calling NewJSONView should also always output a version message, which is a
// convenient way to test that NewJSONView works.
func TestNewJSONView(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
NewJSONView(NewView(streams))
version := tfversion.String()
want := []map[string]interface{}{
{
"@level": "info",
"@message": fmt.Sprintf("Terraform %s", version),
"@module": "terraform.ui",
"type": "version",
"terraform": version,
"ui": JSON_UI_VERSION,
},
}
testJSONViewOutputEqualsFull(t, done(t).Stdout(), want)
}
func TestJSONView_Log(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
jv.Log("hello, world")
want := []map[string]interface{}{
{
"@level": "info",
"@message": "hello, world",
"@module": "terraform.ui",
"type": "log",
},
}
testJSONViewOutputEquals(t, done(t).Stdout(), want)
}
// This test covers only the basics of JSON diagnostic rendering, as more
// complex diagnostics are tested elsewhere.
func TestJSONView_Diagnostics(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
`Improper use of "less"`,
`You probably mean "10 buckets or fewer"`,
))
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unusually stripey cat detected",
"Are you sure this random_pet isn't a cheetah?",
))
jv.Diagnostics(diags)
want := []map[string]interface{}{
{
"@level": "warn",
"@message": `Warning: Improper use of "less"`,
"@module": "terraform.ui",
"type": "diagnostic",
"diagnostic": map[string]interface{}{
"severity": "warning",
"summary": `Improper use of "less"`,
"detail": `You probably mean "10 buckets or fewer"`,
},
},
{
"@level": "error",
"@message": "Error: Unusually stripey cat detected",
"@module": "terraform.ui",
"type": "diagnostic",
"diagnostic": map[string]interface{}{
"severity": "error",
"summary": "Unusually stripey cat detected",
"detail": "Are you sure this random_pet isn't a cheetah?",
},
},
}
testJSONViewOutputEquals(t, done(t).Stdout(), want)
}
func TestJSONView_PlannedChange(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
foo, diags := addrs.ParseModuleInstanceStr("module.foo")
if len(diags) > 0 {
t.Fatal(diags.Err())
}
managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"}
cs := &plans.ResourceInstanceChangeSrc{
Addr: managed.Instance(addrs.StringKey("boop")).Absolute(foo),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
},
}
jv.PlannedChange(viewsjson.NewResourceInstanceChange(cs))
want := []map[string]interface{}{
{
"@level": "info",
"@message": `module.foo.test_instance.bar["boop"]: Plan to create`,
"@module": "terraform.ui",
"type": "planned_change",
"change": map[string]interface{}{
"action": "create",
"resource": map[string]interface{}{
"addr": `module.foo.test_instance.bar["boop"]`,
"implied_provider": "test",
"module": "module.foo",
"resource": `test_instance.bar["boop"]`,
"resource_key": "boop",
"resource_name": "bar",
"resource_type": "test_instance",
},
},
},
}
testJSONViewOutputEquals(t, done(t).Stdout(), want)
}
func TestJSONView_ChangeSummary(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
jv.ChangeSummary(&viewsjson.ChangeSummary{
Add: 1,
Change: 2,
Remove: 3,
Operation: viewsjson.OperationApplied,
})
want := []map[string]interface{}{
{
"@level": "info",
"@message": "Apply complete! Resources: 1 added, 2 changed, 3 destroyed.",
"@module": "terraform.ui",
"type": "change_summary",
"changes": map[string]interface{}{
"add": float64(1),
"change": float64(2),
"remove": float64(3),
"operation": "apply",
},
},
}
testJSONViewOutputEquals(t, done(t).Stdout(), want)
}
func TestJSONView_Hook(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
foo, diags := addrs.ParseModuleInstanceStr("module.foo")
if len(diags) > 0 {
t.Fatal(diags.Err())
}
managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"}
addr := managed.Instance(addrs.StringKey("boop")).Absolute(foo)
hook := viewsjson.NewApplyComplete(addr, plans.Create, "id", "boop-beep", 34*time.Second)
jv.Hook(hook)
want := []map[string]interface{}{
{
"@level": "info",
"@message": `module.foo.test_instance.bar["boop"]: Creation complete after 34s [id=boop-beep]`,
"@module": "terraform.ui",
"type": "apply_complete",
"hook": map[string]interface{}{
"resource": map[string]interface{}{
"addr": `module.foo.test_instance.bar["boop"]`,
"implied_provider": "test",
"module": "module.foo",
"resource": `test_instance.bar["boop"]`,
"resource_key": "boop",
"resource_name": "bar",
"resource_type": "test_instance",
},
"action": "create",
"id_key": "id",
"id_value": "boop-beep",
"elapsed_seconds": float64(34),
},
},
}
testJSONViewOutputEquals(t, done(t).Stdout(), want)
}
func TestJSONView_Outputs(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
jv.Outputs(viewsjson.Outputs{
"boop_count": {
Sensitive: false,
Value: json.RawMessage(`92`),
Type: json.RawMessage(`"number"`),
},
"password": {
Sensitive: true,
Value: json.RawMessage(`"horse-battery"`),
Type: json.RawMessage(`"string"`),
},
})
want := []map[string]interface{}{
{
"@level": "info",
"@message": "Outputs: 2",
"@module": "terraform.ui",
"type": "outputs",
"outputs": map[string]interface{}{
"boop_count": map[string]interface{}{
"sensitive": false,
"value": float64(92),
"type": "number",
},
"password": map[string]interface{}{
"sensitive": true,
"value": "horse-battery",
"type": "string",
},
},
},
}
testJSONViewOutputEquals(t, done(t).Stdout(), want)
}
// This helper function tests a possibly multi-line JSONView output string
// against a slice of structs representing the desired log messages. It
// verifies that the output of JSONView is in JSON log format, one message per
// line.
func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string]interface{}) {
t.Helper()
// Remove final trailing newline
output = strings.TrimSuffix(output, "\n")
// Split log into lines, each of which should be a JSON log message
gotLines := strings.Split(output, "\n")
if len(gotLines) != len(want) {
t.Fatalf("unexpected number of messages. got %d, want %d", len(gotLines), len(want))
}
// Unmarshal each line and compare to the expected value
for i := range gotLines {
var gotStruct map[string]interface{}
wantStruct := want[i]
if err := json.Unmarshal([]byte(gotLines[i]), &gotStruct); err != nil {
t.Fatal(err)
}
if timestamp, ok := gotStruct["@timestamp"]; !ok {
t.Errorf("message has no timestamp: %#v", gotStruct)
} else {
// Remove the timestamp value from the struct to allow comparison
delete(gotStruct, "@timestamp")
// Verify the timestamp format
if _, err := time.Parse("2006-01-02T15:04:05.000000Z07:00", timestamp.(string)); err != nil {
t.Fatalf("error parsing timestamp: %s", err)
}
}
if !cmp.Equal(wantStruct, gotStruct) {
t.Fatalf("unexpected output on line %d:\n%s", i, cmp.Diff(wantStruct, gotStruct))
}
}
}
// testJSONViewOutputEquals skips the first line of output, since it ought to
// be a version message that we don't care about for most of our tests.
func testJSONViewOutputEquals(t *testing.T, output string, want []map[string]interface{}) {
t.Helper()
// Remove up to the first newline
index := strings.Index(output, "\n")
if index >= 0 {
output = output[index+1:]
}
testJSONViewOutputEqualsFull(t, output, want)
}