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 }