github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/views/output_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package views
     5  
     6  import (
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/terramate-io/tf/command/arguments"
    11  	"github.com/terramate-io/tf/states"
    12  	"github.com/terramate-io/tf/terminal"
    13  	"github.com/zclconf/go-cty/cty"
    14  )
    15  
    16  // Test various single output values for human-readable UI. Note that since
    17  // OutputHuman defers to repl.FormatValue to render a single value, most of the
    18  // test coverage should be in that package.
    19  func TestOutputHuman_single(t *testing.T) {
    20  	testCases := map[string]struct {
    21  		value   cty.Value
    22  		want    string
    23  		wantErr bool
    24  	}{
    25  		"string": {
    26  			value: cty.StringVal("hello"),
    27  			want:  "\"hello\"\n",
    28  		},
    29  		"list of maps": {
    30  			value: cty.ListVal([]cty.Value{
    31  				cty.MapVal(map[string]cty.Value{
    32  					"key":  cty.StringVal("value"),
    33  					"key2": cty.StringVal("value2"),
    34  				}),
    35  				cty.MapVal(map[string]cty.Value{
    36  					"key": cty.StringVal("value"),
    37  				}),
    38  			}),
    39  			want: `tolist([
    40    tomap({
    41      "key" = "value"
    42      "key2" = "value2"
    43    }),
    44    tomap({
    45      "key" = "value"
    46    }),
    47  ])
    48  `,
    49  		},
    50  	}
    51  
    52  	for name, tc := range testCases {
    53  		t.Run(name, func(t *testing.T) {
    54  			streams, done := terminal.StreamsForTesting(t)
    55  			v := NewOutput(arguments.ViewHuman, NewView(streams))
    56  
    57  			outputs := map[string]*states.OutputValue{
    58  				"foo": {Value: tc.value},
    59  			}
    60  			diags := v.Output("foo", outputs)
    61  
    62  			if diags.HasErrors() {
    63  				if !tc.wantErr {
    64  					t.Fatalf("unexpected diagnostics: %s", diags)
    65  				}
    66  			} else if tc.wantErr {
    67  				t.Fatalf("succeeded, but want error")
    68  			}
    69  
    70  			if got, want := done(t).Stdout(), tc.want; got != want {
    71  				t.Errorf("wrong result\ngot:  %q\nwant: %q", got, want)
    72  			}
    73  		})
    74  	}
    75  }
    76  
    77  // Sensitive output values are rendered to the console intentionally when
    78  // requesting a single output.
    79  func TestOutput_sensitive(t *testing.T) {
    80  	testCases := map[string]arguments.ViewType{
    81  		"human": arguments.ViewHuman,
    82  		"json":  arguments.ViewJSON,
    83  		"raw":   arguments.ViewRaw,
    84  	}
    85  	for name, vt := range testCases {
    86  		t.Run(name, func(t *testing.T) {
    87  			streams, done := terminal.StreamsForTesting(t)
    88  			v := NewOutput(vt, NewView(streams))
    89  
    90  			outputs := map[string]*states.OutputValue{
    91  				"foo": {
    92  					Value:     cty.StringVal("secret"),
    93  					Sensitive: true,
    94  				},
    95  			}
    96  			diags := v.Output("foo", outputs)
    97  
    98  			if diags.HasErrors() {
    99  				t.Fatalf("unexpected diagnostics: %s", diags)
   100  			}
   101  
   102  			// Test for substring match here because we don't care about exact
   103  			// output format in this test, just the presence of the sensitive
   104  			// value.
   105  			if got, want := done(t).Stdout(), "secret"; !strings.Contains(got, want) {
   106  				t.Errorf("wrong result\ngot:  %q\nwant: %q", got, want)
   107  			}
   108  		})
   109  	}
   110  }
   111  
   112  // Showing all outputs is supported by human and JSON output format.
   113  func TestOutput_all(t *testing.T) {
   114  	outputs := map[string]*states.OutputValue{
   115  		"foo": {
   116  			Value:     cty.StringVal("secret"),
   117  			Sensitive: true,
   118  		},
   119  		"bar": {
   120  			Value: cty.ListVal([]cty.Value{cty.True, cty.False, cty.True}),
   121  		},
   122  		"baz": {
   123  			Value: cty.ObjectVal(map[string]cty.Value{
   124  				"boop": cty.NumberIntVal(5),
   125  				"beep": cty.StringVal("true"),
   126  			}),
   127  		},
   128  	}
   129  
   130  	testCases := map[string]struct {
   131  		vt   arguments.ViewType
   132  		want string
   133  	}{
   134  		"human": {
   135  			arguments.ViewHuman,
   136  			`bar = tolist([
   137    true,
   138    false,
   139    true,
   140  ])
   141  baz = {
   142    "beep" = "true"
   143    "boop" = 5
   144  }
   145  foo = <sensitive>
   146  `,
   147  		},
   148  		"json": {
   149  			arguments.ViewJSON,
   150  			`{
   151    "bar": {
   152      "sensitive": false,
   153      "type": [
   154        "list",
   155        "bool"
   156      ],
   157      "value": [
   158        true,
   159        false,
   160        true
   161      ]
   162    },
   163    "baz": {
   164      "sensitive": false,
   165      "type": [
   166        "object",
   167        {
   168          "beep": "string",
   169          "boop": "number"
   170        }
   171      ],
   172      "value": {
   173        "beep": "true",
   174        "boop": 5
   175      }
   176    },
   177    "foo": {
   178      "sensitive": true,
   179      "type": "string",
   180      "value": "secret"
   181    }
   182  }
   183  `,
   184  		},
   185  	}
   186  
   187  	for name, tc := range testCases {
   188  		t.Run(name, func(t *testing.T) {
   189  			streams, done := terminal.StreamsForTesting(t)
   190  			v := NewOutput(tc.vt, NewView(streams))
   191  			diags := v.Output("", outputs)
   192  
   193  			if diags.HasErrors() {
   194  				t.Fatalf("unexpected diagnostics: %s", diags)
   195  			}
   196  
   197  			if got := done(t).Stdout(); got != tc.want {
   198  				t.Errorf("wrong result\ngot:  %q\nwant: %q", got, tc.want)
   199  			}
   200  		})
   201  	}
   202  }
   203  
   204  // JSON output format supports empty outputs by rendering an empty object
   205  // without diagnostics.
   206  func TestOutputJSON_empty(t *testing.T) {
   207  	streams, done := terminal.StreamsForTesting(t)
   208  	v := NewOutput(arguments.ViewJSON, NewView(streams))
   209  
   210  	diags := v.Output("", map[string]*states.OutputValue{})
   211  
   212  	if diags.HasErrors() {
   213  		t.Fatalf("unexpected diagnostics: %s", diags)
   214  	}
   215  
   216  	if got, want := done(t).Stdout(), "{}\n"; got != want {
   217  		t.Errorf("wrong result\ngot:  %q\nwant: %q", got, want)
   218  	}
   219  }
   220  
   221  // Human and raw formats render a warning if there are no outputs.
   222  func TestOutput_emptyWarning(t *testing.T) {
   223  	testCases := map[string]arguments.ViewType{
   224  		"human": arguments.ViewHuman,
   225  		"raw":   arguments.ViewRaw,
   226  	}
   227  
   228  	for name, vt := range testCases {
   229  		t.Run(name, func(t *testing.T) {
   230  			streams, done := terminal.StreamsForTesting(t)
   231  			v := NewOutput(vt, NewView(streams))
   232  
   233  			diags := v.Output("", map[string]*states.OutputValue{})
   234  
   235  			if got, want := done(t).Stdout(), ""; got != want {
   236  				t.Errorf("wrong result\ngot:  %q\nwant: %q", got, want)
   237  			}
   238  
   239  			if len(diags) != 1 {
   240  				t.Fatalf("expected 1 diagnostic, got %d", len(diags))
   241  			}
   242  
   243  			if diags.HasErrors() {
   244  				t.Fatalf("unexpected error diagnostics: %s", diags)
   245  			}
   246  
   247  			if got, want := diags[0].Description().Summary, "No outputs found"; got != want {
   248  				t.Errorf("unexpected diagnostics: %s", diags)
   249  			}
   250  		})
   251  	}
   252  }
   253  
   254  // Raw output is a simple unquoted output format designed for shell scripts,
   255  // which relies on the cty.AsString() implementation. This test covers
   256  // formatting for supported value types.
   257  func TestOutputRaw(t *testing.T) {
   258  	values := map[string]cty.Value{
   259  		"str":      cty.StringVal("bar"),
   260  		"multistr": cty.StringVal("bar\nbaz"),
   261  		"num":      cty.NumberIntVal(2),
   262  		"bool":     cty.True,
   263  		"obj":      cty.EmptyObjectVal,
   264  		"null":     cty.NullVal(cty.String),
   265  		"unknown":  cty.UnknownVal(cty.String),
   266  	}
   267  
   268  	tests := map[string]struct {
   269  		WantOutput string
   270  		WantErr    bool
   271  	}{
   272  		"str":      {WantOutput: "bar"},
   273  		"multistr": {WantOutput: "bar\nbaz"},
   274  		"num":      {WantOutput: "2"},
   275  		"bool":     {WantOutput: "true"},
   276  		"obj":      {WantErr: true},
   277  		"null":     {WantErr: true},
   278  		"unknown":  {WantErr: true},
   279  	}
   280  
   281  	for name, test := range tests {
   282  		t.Run(name, func(t *testing.T) {
   283  			streams, done := terminal.StreamsForTesting(t)
   284  			v := NewOutput(arguments.ViewRaw, NewView(streams))
   285  
   286  			value := values[name]
   287  			outputs := map[string]*states.OutputValue{
   288  				name: {Value: value},
   289  			}
   290  			diags := v.Output(name, outputs)
   291  
   292  			if diags.HasErrors() {
   293  				if !test.WantErr {
   294  					t.Fatalf("unexpected diagnostics: %s", diags)
   295  				}
   296  			} else if test.WantErr {
   297  				t.Fatalf("succeeded, but want error")
   298  			}
   299  
   300  			if got, want := done(t).Stdout(), test.WantOutput; got != want {
   301  				t.Errorf("wrong result\ngot:  %q\nwant: %q", got, want)
   302  			}
   303  		})
   304  	}
   305  }
   306  
   307  // Raw cannot render all outputs.
   308  func TestOutputRaw_all(t *testing.T) {
   309  	streams, done := terminal.StreamsForTesting(t)
   310  	v := NewOutput(arguments.ViewRaw, NewView(streams))
   311  
   312  	outputs := map[string]*states.OutputValue{
   313  		"foo": {Value: cty.StringVal("secret")},
   314  		"bar": {Value: cty.True},
   315  	}
   316  	diags := v.Output("", outputs)
   317  
   318  	if got, want := done(t).Stdout(), ""; got != want {
   319  		t.Errorf("wrong result\ngot:  %q\nwant: %q", got, want)
   320  	}
   321  
   322  	if !diags.HasErrors() {
   323  		t.Fatalf("expected diagnostics, got %s", diags)
   324  	}
   325  
   326  	if got, want := diags.Err().Error(), "Raw output format is only supported for single outputs"; got != want {
   327  		t.Errorf("unexpected diagnostics: %s", diags)
   328  	}
   329  }
   330  
   331  // All outputs render an error if a specific output is requested which is
   332  // missing from the map of outputs.
   333  func TestOutput_missing(t *testing.T) {
   334  	testCases := map[string]arguments.ViewType{
   335  		"human": arguments.ViewHuman,
   336  		"json":  arguments.ViewJSON,
   337  		"raw":   arguments.ViewRaw,
   338  	}
   339  
   340  	for name, vt := range testCases {
   341  		t.Run(name, func(t *testing.T) {
   342  			streams, done := terminal.StreamsForTesting(t)
   343  			v := NewOutput(vt, NewView(streams))
   344  
   345  			diags := v.Output("foo", map[string]*states.OutputValue{
   346  				"bar": {Value: cty.StringVal("boop")},
   347  			})
   348  
   349  			if len(diags) != 1 {
   350  				t.Fatalf("expected 1 diagnostic, got %d", len(diags))
   351  			}
   352  
   353  			if !diags.HasErrors() {
   354  				t.Fatalf("expected error diagnostics, got %s", diags)
   355  			}
   356  
   357  			if got, want := diags[0].Description().Summary, `Output "foo" not found`; got != want {
   358  				t.Errorf("unexpected diagnostics: %s", diags)
   359  			}
   360  
   361  			if got, want := done(t).Stdout(), ""; got != want {
   362  				t.Errorf("wrong result\ngot:  %q\nwant: %q", got, want)
   363  			}
   364  		})
   365  	}
   366  }