github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/command/views/json_view_test.go (about)

     1  package views
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"strings"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/google/go-cmp/cmp"
    11  	"github.com/hashicorp/terraform/internal/addrs"
    12  	viewsjson "github.com/hashicorp/terraform/internal/command/views/json"
    13  	"github.com/hashicorp/terraform/internal/plans"
    14  	"github.com/hashicorp/terraform/internal/terminal"
    15  	"github.com/hashicorp/terraform/internal/tfdiags"
    16  	tfversion "github.com/hashicorp/terraform/version"
    17  )
    18  
    19  // Calling NewJSONView should also always output a version message, which is a
    20  // convenient way to test that NewJSONView works.
    21  func TestNewJSONView(t *testing.T) {
    22  	streams, done := terminal.StreamsForTesting(t)
    23  	NewJSONView(NewView(streams))
    24  
    25  	version := tfversion.String()
    26  	want := []map[string]interface{}{
    27  		{
    28  			"@level":    "info",
    29  			"@message":  fmt.Sprintf("Terraform %s", version),
    30  			"@module":   "terraform.ui",
    31  			"type":      "version",
    32  			"terraform": version,
    33  			"ui":        JSON_UI_VERSION,
    34  		},
    35  	}
    36  
    37  	testJSONViewOutputEqualsFull(t, done(t).Stdout(), want)
    38  }
    39  
    40  func TestJSONView_Log(t *testing.T) {
    41  	streams, done := terminal.StreamsForTesting(t)
    42  	jv := NewJSONView(NewView(streams))
    43  
    44  	jv.Log("hello, world")
    45  
    46  	want := []map[string]interface{}{
    47  		{
    48  			"@level":   "info",
    49  			"@message": "hello, world",
    50  			"@module":  "terraform.ui",
    51  			"type":     "log",
    52  		},
    53  	}
    54  	testJSONViewOutputEquals(t, done(t).Stdout(), want)
    55  }
    56  
    57  // This test covers only the basics of JSON diagnostic rendering, as more
    58  // complex diagnostics are tested elsewhere.
    59  func TestJSONView_Diagnostics(t *testing.T) {
    60  	streams, done := terminal.StreamsForTesting(t)
    61  	jv := NewJSONView(NewView(streams))
    62  
    63  	var diags tfdiags.Diagnostics
    64  	diags = diags.Append(tfdiags.Sourceless(
    65  		tfdiags.Warning,
    66  		`Improper use of "less"`,
    67  		`You probably mean "10 buckets or fewer"`,
    68  	))
    69  	diags = diags.Append(tfdiags.Sourceless(
    70  		tfdiags.Error,
    71  		"Unusually stripey cat detected",
    72  		"Are you sure this random_pet isn't a cheetah?",
    73  	))
    74  
    75  	jv.Diagnostics(diags)
    76  
    77  	want := []map[string]interface{}{
    78  		{
    79  			"@level":   "warn",
    80  			"@message": `Warning: Improper use of "less"`,
    81  			"@module":  "terraform.ui",
    82  			"type":     "diagnostic",
    83  			"diagnostic": map[string]interface{}{
    84  				"severity": "warning",
    85  				"summary":  `Improper use of "less"`,
    86  				"detail":   `You probably mean "10 buckets or fewer"`,
    87  			},
    88  		},
    89  		{
    90  			"@level":   "error",
    91  			"@message": "Error: Unusually stripey cat detected",
    92  			"@module":  "terraform.ui",
    93  			"type":     "diagnostic",
    94  			"diagnostic": map[string]interface{}{
    95  				"severity": "error",
    96  				"summary":  "Unusually stripey cat detected",
    97  				"detail":   "Are you sure this random_pet isn't a cheetah?",
    98  			},
    99  		},
   100  	}
   101  	testJSONViewOutputEquals(t, done(t).Stdout(), want)
   102  }
   103  
   104  func TestJSONView_PlannedChange(t *testing.T) {
   105  	streams, done := terminal.StreamsForTesting(t)
   106  	jv := NewJSONView(NewView(streams))
   107  
   108  	foo, diags := addrs.ParseModuleInstanceStr("module.foo")
   109  	if len(diags) > 0 {
   110  		t.Fatal(diags.Err())
   111  	}
   112  	managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"}
   113  	cs := &plans.ResourceInstanceChangeSrc{
   114  		Addr: managed.Instance(addrs.StringKey("boop")).Absolute(foo),
   115  		ChangeSrc: plans.ChangeSrc{
   116  			Action: plans.Create,
   117  		},
   118  	}
   119  	jv.PlannedChange(viewsjson.NewResourceInstanceChange(cs))
   120  
   121  	want := []map[string]interface{}{
   122  		{
   123  			"@level":   "info",
   124  			"@message": `module.foo.test_instance.bar["boop"]: Plan to create`,
   125  			"@module":  "terraform.ui",
   126  			"type":     "planned_change",
   127  			"change": map[string]interface{}{
   128  				"action": "create",
   129  				"resource": map[string]interface{}{
   130  					"addr":             `module.foo.test_instance.bar["boop"]`,
   131  					"implied_provider": "test",
   132  					"module":           "module.foo",
   133  					"resource":         `test_instance.bar["boop"]`,
   134  					"resource_key":     "boop",
   135  					"resource_name":    "bar",
   136  					"resource_type":    "test_instance",
   137  				},
   138  			},
   139  		},
   140  	}
   141  	testJSONViewOutputEquals(t, done(t).Stdout(), want)
   142  }
   143  
   144  func TestJSONView_ResourceDrift(t *testing.T) {
   145  	streams, done := terminal.StreamsForTesting(t)
   146  	jv := NewJSONView(NewView(streams))
   147  
   148  	foo, diags := addrs.ParseModuleInstanceStr("module.foo")
   149  	if len(diags) > 0 {
   150  		t.Fatal(diags.Err())
   151  	}
   152  	managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"}
   153  	cs := &plans.ResourceInstanceChangeSrc{
   154  		Addr: managed.Instance(addrs.StringKey("boop")).Absolute(foo),
   155  		ChangeSrc: plans.ChangeSrc{
   156  			Action: plans.Update,
   157  		},
   158  	}
   159  	jv.ResourceDrift(viewsjson.NewResourceInstanceChange(cs))
   160  
   161  	want := []map[string]interface{}{
   162  		{
   163  			"@level":   "info",
   164  			"@message": `module.foo.test_instance.bar["boop"]: Drift detected (update)`,
   165  			"@module":  "terraform.ui",
   166  			"type":     "resource_drift",
   167  			"change": map[string]interface{}{
   168  				"action": "update",
   169  				"resource": map[string]interface{}{
   170  					"addr":             `module.foo.test_instance.bar["boop"]`,
   171  					"implied_provider": "test",
   172  					"module":           "module.foo",
   173  					"resource":         `test_instance.bar["boop"]`,
   174  					"resource_key":     "boop",
   175  					"resource_name":    "bar",
   176  					"resource_type":    "test_instance",
   177  				},
   178  			},
   179  		},
   180  	}
   181  	testJSONViewOutputEquals(t, done(t).Stdout(), want)
   182  }
   183  
   184  func TestJSONView_ChangeSummary(t *testing.T) {
   185  	streams, done := terminal.StreamsForTesting(t)
   186  	jv := NewJSONView(NewView(streams))
   187  
   188  	jv.ChangeSummary(&viewsjson.ChangeSummary{
   189  		Add:       1,
   190  		Change:    2,
   191  		Remove:    3,
   192  		Operation: viewsjson.OperationApplied,
   193  	})
   194  
   195  	want := []map[string]interface{}{
   196  		{
   197  			"@level":   "info",
   198  			"@message": "Apply complete! Resources: 1 added, 2 changed, 3 destroyed.",
   199  			"@module":  "terraform.ui",
   200  			"type":     "change_summary",
   201  			"changes": map[string]interface{}{
   202  				"add":       float64(1),
   203  				"change":    float64(2),
   204  				"remove":    float64(3),
   205  				"operation": "apply",
   206  			},
   207  		},
   208  	}
   209  	testJSONViewOutputEquals(t, done(t).Stdout(), want)
   210  }
   211  
   212  func TestJSONView_Hook(t *testing.T) {
   213  	streams, done := terminal.StreamsForTesting(t)
   214  	jv := NewJSONView(NewView(streams))
   215  
   216  	foo, diags := addrs.ParseModuleInstanceStr("module.foo")
   217  	if len(diags) > 0 {
   218  		t.Fatal(diags.Err())
   219  	}
   220  	managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"}
   221  	addr := managed.Instance(addrs.StringKey("boop")).Absolute(foo)
   222  	hook := viewsjson.NewApplyComplete(addr, plans.Create, "id", "boop-beep", 34*time.Second)
   223  
   224  	jv.Hook(hook)
   225  
   226  	want := []map[string]interface{}{
   227  		{
   228  			"@level":   "info",
   229  			"@message": `module.foo.test_instance.bar["boop"]: Creation complete after 34s [id=boop-beep]`,
   230  			"@module":  "terraform.ui",
   231  			"type":     "apply_complete",
   232  			"hook": map[string]interface{}{
   233  				"resource": map[string]interface{}{
   234  					"addr":             `module.foo.test_instance.bar["boop"]`,
   235  					"implied_provider": "test",
   236  					"module":           "module.foo",
   237  					"resource":         `test_instance.bar["boop"]`,
   238  					"resource_key":     "boop",
   239  					"resource_name":    "bar",
   240  					"resource_type":    "test_instance",
   241  				},
   242  				"action":          "create",
   243  				"id_key":          "id",
   244  				"id_value":        "boop-beep",
   245  				"elapsed_seconds": float64(34),
   246  			},
   247  		},
   248  	}
   249  	testJSONViewOutputEquals(t, done(t).Stdout(), want)
   250  }
   251  
   252  func TestJSONView_Outputs(t *testing.T) {
   253  	streams, done := terminal.StreamsForTesting(t)
   254  	jv := NewJSONView(NewView(streams))
   255  
   256  	jv.Outputs(viewsjson.Outputs{
   257  		"boop_count": {
   258  			Sensitive: false,
   259  			Value:     json.RawMessage(`92`),
   260  			Type:      json.RawMessage(`"number"`),
   261  		},
   262  		"password": {
   263  			Sensitive: true,
   264  			Value:     json.RawMessage(`"horse-battery"`),
   265  			Type:      json.RawMessage(`"string"`),
   266  		},
   267  	})
   268  
   269  	want := []map[string]interface{}{
   270  		{
   271  			"@level":   "info",
   272  			"@message": "Outputs: 2",
   273  			"@module":  "terraform.ui",
   274  			"type":     "outputs",
   275  			"outputs": map[string]interface{}{
   276  				"boop_count": map[string]interface{}{
   277  					"sensitive": false,
   278  					"value":     float64(92),
   279  					"type":      "number",
   280  				},
   281  				"password": map[string]interface{}{
   282  					"sensitive": true,
   283  					"value":     "horse-battery",
   284  					"type":      "string",
   285  				},
   286  			},
   287  		},
   288  	}
   289  	testJSONViewOutputEquals(t, done(t).Stdout(), want)
   290  }
   291  
   292  // This helper function tests a possibly multi-line JSONView output string
   293  // against a slice of structs representing the desired log messages. It
   294  // verifies that the output of JSONView is in JSON log format, one message per
   295  // line.
   296  func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string]interface{}) {
   297  	t.Helper()
   298  
   299  	// Remove final trailing newline
   300  	output = strings.TrimSuffix(output, "\n")
   301  
   302  	// Split log into lines, each of which should be a JSON log message
   303  	gotLines := strings.Split(output, "\n")
   304  
   305  	if len(gotLines) != len(want) {
   306  		t.Errorf("unexpected number of messages. got %d, want %d", len(gotLines), len(want))
   307  	}
   308  
   309  	// Unmarshal each line and compare to the expected value
   310  	for i := range gotLines {
   311  		var gotStruct map[string]interface{}
   312  		if i >= len(want) {
   313  			t.Error("reached end of want messages too soon")
   314  			break
   315  		}
   316  		wantStruct := want[i]
   317  
   318  		if err := json.Unmarshal([]byte(gotLines[i]), &gotStruct); err != nil {
   319  			t.Fatal(err)
   320  		}
   321  
   322  		if timestamp, ok := gotStruct["@timestamp"]; !ok {
   323  			t.Errorf("message has no timestamp: %#v", gotStruct)
   324  		} else {
   325  			// Remove the timestamp value from the struct to allow comparison
   326  			delete(gotStruct, "@timestamp")
   327  
   328  			// Verify the timestamp format
   329  			if _, err := time.Parse("2006-01-02T15:04:05.000000Z07:00", timestamp.(string)); err != nil {
   330  				t.Errorf("error parsing timestamp on line %d: %s", i, err)
   331  			}
   332  		}
   333  
   334  		if !cmp.Equal(wantStruct, gotStruct) {
   335  			t.Errorf("unexpected output on line %d:\n%s", i, cmp.Diff(wantStruct, gotStruct))
   336  		}
   337  	}
   338  }
   339  
   340  // testJSONViewOutputEquals skips the first line of output, since it ought to
   341  // be a version message that we don't care about for most of our tests.
   342  func testJSONViewOutputEquals(t *testing.T, output string, want []map[string]interface{}) {
   343  	t.Helper()
   344  
   345  	// Remove up to the first newline
   346  	index := strings.Index(output, "\n")
   347  	if index >= 0 {
   348  		output = output[index+1:]
   349  	}
   350  	testJSONViewOutputEqualsFull(t, output, want)
   351  }