| package views |
| |
| import ( |
| "strings" |
| "testing" |
| |
| "github.com/hashicorp/terraform/internal/command/arguments" |
| "github.com/hashicorp/terraform/internal/states" |
| "github.com/hashicorp/terraform/internal/terminal" |
| "github.com/zclconf/go-cty/cty" |
| ) |
| |
| // Test various single output values for human-readable UI. Note that since |
| // OutputHuman defers to repl.FormatValue to render a single value, most of the |
| // test coverage should be in that package. |
| func TestOutputHuman_single(t *testing.T) { |
| testCases := map[string]struct { |
| value cty.Value |
| want string |
| wantErr bool |
| }{ |
| "string": { |
| value: cty.StringVal("hello"), |
| want: "\"hello\"\n", |
| }, |
| "list of maps": { |
| value: cty.ListVal([]cty.Value{ |
| cty.MapVal(map[string]cty.Value{ |
| "key": cty.StringVal("value"), |
| "key2": cty.StringVal("value2"), |
| }), |
| cty.MapVal(map[string]cty.Value{ |
| "key": cty.StringVal("value"), |
| }), |
| }), |
| want: `tolist([ |
| tomap({ |
| "key" = "value" |
| "key2" = "value2" |
| }), |
| tomap({ |
| "key" = "value" |
| }), |
| ]) |
| `, |
| }, |
| } |
| |
| for name, tc := range testCases { |
| t.Run(name, func(t *testing.T) { |
| streams, done := terminal.StreamsForTesting(t) |
| v := NewOutput(arguments.ViewHuman, NewView(streams)) |
| |
| outputs := map[string]*states.OutputValue{ |
| "foo": {Value: tc.value}, |
| } |
| diags := v.Output("foo", outputs) |
| |
| if diags.HasErrors() { |
| if !tc.wantErr { |
| t.Fatalf("unexpected diagnostics: %s", diags) |
| } |
| } else if tc.wantErr { |
| t.Fatalf("succeeded, but want error") |
| } |
| |
| if got, want := done(t).Stdout(), tc.want; got != want { |
| t.Errorf("wrong result\ngot: %q\nwant: %q", got, want) |
| } |
| }) |
| } |
| } |
| |
| // Sensitive output values are rendered to the console intentionally when |
| // requesting a single output. |
| func TestOutput_sensitive(t *testing.T) { |
| testCases := map[string]arguments.ViewType{ |
| "human": arguments.ViewHuman, |
| "json": arguments.ViewJSON, |
| "raw": arguments.ViewRaw, |
| } |
| for name, vt := range testCases { |
| t.Run(name, func(t *testing.T) { |
| streams, done := terminal.StreamsForTesting(t) |
| v := NewOutput(vt, NewView(streams)) |
| |
| outputs := map[string]*states.OutputValue{ |
| "foo": { |
| Value: cty.StringVal("secret"), |
| Sensitive: true, |
| }, |
| } |
| diags := v.Output("foo", outputs) |
| |
| if diags.HasErrors() { |
| t.Fatalf("unexpected diagnostics: %s", diags) |
| } |
| |
| // Test for substring match here because we don't care about exact |
| // output format in this test, just the presence of the sensitive |
| // value. |
| if got, want := done(t).Stdout(), "secret"; !strings.Contains(got, want) { |
| t.Errorf("wrong result\ngot: %q\nwant: %q", got, want) |
| } |
| }) |
| } |
| } |
| |
| // Showing all outputs is supported by human and JSON output format. |
| func TestOutput_all(t *testing.T) { |
| outputs := map[string]*states.OutputValue{ |
| "foo": { |
| Value: cty.StringVal("secret"), |
| Sensitive: true, |
| }, |
| "bar": { |
| Value: cty.ListVal([]cty.Value{cty.True, cty.False, cty.True}), |
| }, |
| "baz": { |
| Value: cty.ObjectVal(map[string]cty.Value{ |
| "boop": cty.NumberIntVal(5), |
| "beep": cty.StringVal("true"), |
| }), |
| }, |
| } |
| |
| testCases := map[string]struct { |
| vt arguments.ViewType |
| want string |
| }{ |
| "human": { |
| arguments.ViewHuman, |
| `bar = tolist([ |
| true, |
| false, |
| true, |
| ]) |
| baz = { |
| "beep" = "true" |
| "boop" = 5 |
| } |
| foo = <sensitive> |
| `, |
| }, |
| "json": { |
| arguments.ViewJSON, |
| `{ |
| "bar": { |
| "sensitive": false, |
| "type": [ |
| "list", |
| "bool" |
| ], |
| "value": [ |
| true, |
| false, |
| true |
| ] |
| }, |
| "baz": { |
| "sensitive": false, |
| "type": [ |
| "object", |
| { |
| "beep": "string", |
| "boop": "number" |
| } |
| ], |
| "value": { |
| "beep": "true", |
| "boop": 5 |
| } |
| }, |
| "foo": { |
| "sensitive": true, |
| "type": "string", |
| "value": "secret" |
| } |
| } |
| `, |
| }, |
| } |
| |
| for name, tc := range testCases { |
| t.Run(name, func(t *testing.T) { |
| streams, done := terminal.StreamsForTesting(t) |
| v := NewOutput(tc.vt, NewView(streams)) |
| diags := v.Output("", outputs) |
| |
| if diags.HasErrors() { |
| t.Fatalf("unexpected diagnostics: %s", diags) |
| } |
| |
| if got := done(t).Stdout(); got != tc.want { |
| t.Errorf("wrong result\ngot: %q\nwant: %q", got, tc.want) |
| } |
| }) |
| } |
| } |
| |
| // JSON output format supports empty outputs by rendering an empty object |
| // without diagnostics. |
| func TestOutputJSON_empty(t *testing.T) { |
| streams, done := terminal.StreamsForTesting(t) |
| v := NewOutput(arguments.ViewJSON, NewView(streams)) |
| |
| diags := v.Output("", map[string]*states.OutputValue{}) |
| |
| if diags.HasErrors() { |
| t.Fatalf("unexpected diagnostics: %s", diags) |
| } |
| |
| if got, want := done(t).Stdout(), "{}\n"; got != want { |
| t.Errorf("wrong result\ngot: %q\nwant: %q", got, want) |
| } |
| } |
| |
| // Human and raw formats render a warning if there are no outputs. |
| func TestOutput_emptyWarning(t *testing.T) { |
| testCases := map[string]arguments.ViewType{ |
| "human": arguments.ViewHuman, |
| "raw": arguments.ViewRaw, |
| } |
| |
| for name, vt := range testCases { |
| t.Run(name, func(t *testing.T) { |
| streams, done := terminal.StreamsForTesting(t) |
| v := NewOutput(vt, NewView(streams)) |
| |
| diags := v.Output("", map[string]*states.OutputValue{}) |
| |
| if got, want := done(t).Stdout(), ""; got != want { |
| t.Errorf("wrong result\ngot: %q\nwant: %q", got, want) |
| } |
| |
| if len(diags) != 1 { |
| t.Fatalf("expected 1 diagnostic, got %d", len(diags)) |
| } |
| |
| if diags.HasErrors() { |
| t.Fatalf("unexpected error diagnostics: %s", diags) |
| } |
| |
| if got, want := diags[0].Description().Summary, "No outputs found"; got != want { |
| t.Errorf("unexpected diagnostics: %s", diags) |
| } |
| }) |
| } |
| } |
| |
| // Raw output is a simple unquoted output format designed for shell scripts, |
| // which relies on the cty.AsString() implementation. This test covers |
| // formatting for supported value types. |
| func TestOutputRaw(t *testing.T) { |
| values := map[string]cty.Value{ |
| "str": cty.StringVal("bar"), |
| "multistr": cty.StringVal("bar\nbaz"), |
| "num": cty.NumberIntVal(2), |
| "bool": cty.True, |
| "obj": cty.EmptyObjectVal, |
| "null": cty.NullVal(cty.String), |
| "unknown": cty.UnknownVal(cty.String), |
| } |
| |
| tests := map[string]struct { |
| WantOutput string |
| WantErr bool |
| }{ |
| "str": {WantOutput: "bar"}, |
| "multistr": {WantOutput: "bar\nbaz"}, |
| "num": {WantOutput: "2"}, |
| "bool": {WantOutput: "true"}, |
| "obj": {WantErr: true}, |
| "null": {WantErr: true}, |
| "unknown": {WantErr: true}, |
| } |
| |
| for name, test := range tests { |
| t.Run(name, func(t *testing.T) { |
| streams, done := terminal.StreamsForTesting(t) |
| v := NewOutput(arguments.ViewRaw, NewView(streams)) |
| |
| value := values[name] |
| outputs := map[string]*states.OutputValue{ |
| name: {Value: value}, |
| } |
| diags := v.Output(name, outputs) |
| |
| if diags.HasErrors() { |
| if !test.WantErr { |
| t.Fatalf("unexpected diagnostics: %s", diags) |
| } |
| } else if test.WantErr { |
| t.Fatalf("succeeded, but want error") |
| } |
| |
| if got, want := done(t).Stdout(), test.WantOutput; got != want { |
| t.Errorf("wrong result\ngot: %q\nwant: %q", got, want) |
| } |
| }) |
| } |
| } |
| |
| // Raw cannot render all outputs. |
| func TestOutputRaw_all(t *testing.T) { |
| streams, done := terminal.StreamsForTesting(t) |
| v := NewOutput(arguments.ViewRaw, NewView(streams)) |
| |
| outputs := map[string]*states.OutputValue{ |
| "foo": {Value: cty.StringVal("secret")}, |
| "bar": {Value: cty.True}, |
| } |
| diags := v.Output("", outputs) |
| |
| if got, want := done(t).Stdout(), ""; got != want { |
| t.Errorf("wrong result\ngot: %q\nwant: %q", got, want) |
| } |
| |
| if !diags.HasErrors() { |
| t.Fatalf("expected diagnostics, got %s", diags) |
| } |
| |
| if got, want := diags.Err().Error(), "Raw output format is only supported for single outputs"; got != want { |
| t.Errorf("unexpected diagnostics: %s", diags) |
| } |
| } |
| |
| // All outputs render an error if a specific output is requested which is |
| // missing from the map of outputs. |
| func TestOutput_missing(t *testing.T) { |
| testCases := map[string]arguments.ViewType{ |
| "human": arguments.ViewHuman, |
| "json": arguments.ViewJSON, |
| "raw": arguments.ViewRaw, |
| } |
| |
| for name, vt := range testCases { |
| t.Run(name, func(t *testing.T) { |
| streams, done := terminal.StreamsForTesting(t) |
| v := NewOutput(vt, NewView(streams)) |
| |
| diags := v.Output("foo", map[string]*states.OutputValue{ |
| "bar": {Value: cty.StringVal("boop")}, |
| }) |
| |
| if len(diags) != 1 { |
| t.Fatalf("expected 1 diagnostic, got %d", len(diags)) |
| } |
| |
| if !diags.HasErrors() { |
| t.Fatalf("expected error diagnostics, got %s", diags) |
| } |
| |
| if got, want := diags[0].Description().Summary, `Output "foo" not found`; got != want { |
| t.Errorf("unexpected diagnostics: %s", diags) |
| } |
| |
| if got, want := done(t).Stdout(), ""; got != want { |
| t.Errorf("wrong result\ngot: %q\nwant: %q", got, want) |
| } |
| }) |
| } |
| } |