github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/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/cycloidio/terraform/addrs"
    12  	viewsjson "github.com/cycloidio/terraform/command/views/json"
    13  	"github.com/cycloidio/terraform/plans"
    14  	"github.com/cycloidio/terraform/terminal"
    15  	"github.com/cycloidio/terraform/tfdiags"
    16  	tfversion "github.com/cycloidio/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  		PrevRunAddr: managed.Instance(addrs.StringKey("boop")).Absolute(foo),
   116  		ChangeSrc: plans.ChangeSrc{
   117  			Action: plans.Create,
   118  		},
   119  	}
   120  	jv.PlannedChange(viewsjson.NewResourceInstanceChange(cs))
   121  
   122  	want := []map[string]interface{}{
   123  		{
   124  			"@level":   "info",
   125  			"@message": `module.foo.test_instance.bar["boop"]: Plan to create`,
   126  			"@module":  "terraform.ui",
   127  			"type":     "planned_change",
   128  			"change": map[string]interface{}{
   129  				"action": "create",
   130  				"resource": map[string]interface{}{
   131  					"addr":             `module.foo.test_instance.bar["boop"]`,
   132  					"implied_provider": "test",
   133  					"module":           "module.foo",
   134  					"resource":         `test_instance.bar["boop"]`,
   135  					"resource_key":     "boop",
   136  					"resource_name":    "bar",
   137  					"resource_type":    "test_instance",
   138  				},
   139  			},
   140  		},
   141  	}
   142  	testJSONViewOutputEquals(t, done(t).Stdout(), want)
   143  }
   144  
   145  func TestJSONView_ResourceDrift(t *testing.T) {
   146  	streams, done := terminal.StreamsForTesting(t)
   147  	jv := NewJSONView(NewView(streams))
   148  
   149  	foo, diags := addrs.ParseModuleInstanceStr("module.foo")
   150  	if len(diags) > 0 {
   151  		t.Fatal(diags.Err())
   152  	}
   153  	managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"}
   154  	cs := &plans.ResourceInstanceChangeSrc{
   155  		Addr:        managed.Instance(addrs.StringKey("boop")).Absolute(foo),
   156  		PrevRunAddr: managed.Instance(addrs.StringKey("boop")).Absolute(foo),
   157  		ChangeSrc: plans.ChangeSrc{
   158  			Action: plans.Update,
   159  		},
   160  	}
   161  	jv.ResourceDrift(viewsjson.NewResourceInstanceChange(cs))
   162  
   163  	want := []map[string]interface{}{
   164  		{
   165  			"@level":   "info",
   166  			"@message": `module.foo.test_instance.bar["boop"]: Drift detected (update)`,
   167  			"@module":  "terraform.ui",
   168  			"type":     "resource_drift",
   169  			"change": map[string]interface{}{
   170  				"action": "update",
   171  				"resource": map[string]interface{}{
   172  					"addr":             `module.foo.test_instance.bar["boop"]`,
   173  					"implied_provider": "test",
   174  					"module":           "module.foo",
   175  					"resource":         `test_instance.bar["boop"]`,
   176  					"resource_key":     "boop",
   177  					"resource_name":    "bar",
   178  					"resource_type":    "test_instance",
   179  				},
   180  			},
   181  		},
   182  	}
   183  	testJSONViewOutputEquals(t, done(t).Stdout(), want)
   184  }
   185  
   186  func TestJSONView_ChangeSummary(t *testing.T) {
   187  	streams, done := terminal.StreamsForTesting(t)
   188  	jv := NewJSONView(NewView(streams))
   189  
   190  	jv.ChangeSummary(&viewsjson.ChangeSummary{
   191  		Add:       1,
   192  		Change:    2,
   193  		Remove:    3,
   194  		Operation: viewsjson.OperationApplied,
   195  	})
   196  
   197  	want := []map[string]interface{}{
   198  		{
   199  			"@level":   "info",
   200  			"@message": "Apply complete! Resources: 1 added, 2 changed, 3 destroyed.",
   201  			"@module":  "terraform.ui",
   202  			"type":     "change_summary",
   203  			"changes": map[string]interface{}{
   204  				"add":       float64(1),
   205  				"change":    float64(2),
   206  				"remove":    float64(3),
   207  				"operation": "apply",
   208  			},
   209  		},
   210  	}
   211  	testJSONViewOutputEquals(t, done(t).Stdout(), want)
   212  }
   213  
   214  func TestJSONView_Hook(t *testing.T) {
   215  	streams, done := terminal.StreamsForTesting(t)
   216  	jv := NewJSONView(NewView(streams))
   217  
   218  	foo, diags := addrs.ParseModuleInstanceStr("module.foo")
   219  	if len(diags) > 0 {
   220  		t.Fatal(diags.Err())
   221  	}
   222  	managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"}
   223  	addr := managed.Instance(addrs.StringKey("boop")).Absolute(foo)
   224  	hook := viewsjson.NewApplyComplete(addr, plans.Create, "id", "boop-beep", 34*time.Second)
   225  
   226  	jv.Hook(hook)
   227  
   228  	want := []map[string]interface{}{
   229  		{
   230  			"@level":   "info",
   231  			"@message": `module.foo.test_instance.bar["boop"]: Creation complete after 34s [id=boop-beep]`,
   232  			"@module":  "terraform.ui",
   233  			"type":     "apply_complete",
   234  			"hook": map[string]interface{}{
   235  				"resource": map[string]interface{}{
   236  					"addr":             `module.foo.test_instance.bar["boop"]`,
   237  					"implied_provider": "test",
   238  					"module":           "module.foo",
   239  					"resource":         `test_instance.bar["boop"]`,
   240  					"resource_key":     "boop",
   241  					"resource_name":    "bar",
   242  					"resource_type":    "test_instance",
   243  				},
   244  				"action":          "create",
   245  				"id_key":          "id",
   246  				"id_value":        "boop-beep",
   247  				"elapsed_seconds": float64(34),
   248  			},
   249  		},
   250  	}
   251  	testJSONViewOutputEquals(t, done(t).Stdout(), want)
   252  }
   253  
   254  func TestJSONView_Outputs(t *testing.T) {
   255  	streams, done := terminal.StreamsForTesting(t)
   256  	jv := NewJSONView(NewView(streams))
   257  
   258  	jv.Outputs(viewsjson.Outputs{
   259  		"boop_count": {
   260  			Sensitive: false,
   261  			Value:     json.RawMessage(`92`),
   262  			Type:      json.RawMessage(`"number"`),
   263  		},
   264  		"password": {
   265  			Sensitive: true,
   266  			Value:     json.RawMessage(`"horse-battery"`),
   267  			Type:      json.RawMessage(`"string"`),
   268  		},
   269  	})
   270  
   271  	want := []map[string]interface{}{
   272  		{
   273  			"@level":   "info",
   274  			"@message": "Outputs: 2",
   275  			"@module":  "terraform.ui",
   276  			"type":     "outputs",
   277  			"outputs": map[string]interface{}{
   278  				"boop_count": map[string]interface{}{
   279  					"sensitive": false,
   280  					"value":     float64(92),
   281  					"type":      "number",
   282  				},
   283  				"password": map[string]interface{}{
   284  					"sensitive": true,
   285  					"value":     "horse-battery",
   286  					"type":      "string",
   287  				},
   288  			},
   289  		},
   290  	}
   291  	testJSONViewOutputEquals(t, done(t).Stdout(), want)
   292  }
   293  
   294  // This helper function tests a possibly multi-line JSONView output string
   295  // against a slice of structs representing the desired log messages. It
   296  // verifies that the output of JSONView is in JSON log format, one message per
   297  // line.
   298  func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string]interface{}) {
   299  	t.Helper()
   300  
   301  	// Remove final trailing newline
   302  	output = strings.TrimSuffix(output, "\n")
   303  
   304  	// Split log into lines, each of which should be a JSON log message
   305  	gotLines := strings.Split(output, "\n")
   306  
   307  	if len(gotLines) != len(want) {
   308  		t.Errorf("unexpected number of messages. got %d, want %d", len(gotLines), len(want))
   309  	}
   310  
   311  	// Unmarshal each line and compare to the expected value
   312  	for i := range gotLines {
   313  		var gotStruct map[string]interface{}
   314  		if i >= len(want) {
   315  			t.Error("reached end of want messages too soon")
   316  			break
   317  		}
   318  		wantStruct := want[i]
   319  
   320  		if err := json.Unmarshal([]byte(gotLines[i]), &gotStruct); err != nil {
   321  			t.Fatal(err)
   322  		}
   323  
   324  		if timestamp, ok := gotStruct["@timestamp"]; !ok {
   325  			t.Errorf("message has no timestamp: %#v", gotStruct)
   326  		} else {
   327  			// Remove the timestamp value from the struct to allow comparison
   328  			delete(gotStruct, "@timestamp")
   329  
   330  			// Verify the timestamp format
   331  			if _, err := time.Parse("2006-01-02T15:04:05.000000Z07:00", timestamp.(string)); err != nil {
   332  				t.Errorf("error parsing timestamp on line %d: %s", i, err)
   333  			}
   334  		}
   335  
   336  		if !cmp.Equal(wantStruct, gotStruct) {
   337  			t.Errorf("unexpected output on line %d:\n%s", i, cmp.Diff(wantStruct, gotStruct))
   338  		}
   339  	}
   340  }
   341  
   342  // testJSONViewOutputEquals skips the first line of output, since it ought to
   343  // be a version message that we don't care about for most of our tests.
   344  func testJSONViewOutputEquals(t *testing.T, output string, want []map[string]interface{}) {
   345  	t.Helper()
   346  
   347  	// Remove up to the first newline
   348  	index := strings.Index(output, "\n")
   349  	if index >= 0 {
   350  		output = output[index+1:]
   351  	}
   352  	testJSONViewOutputEqualsFull(t, output, want)
   353  }