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

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package views
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/terramate-io/tf/command/arguments"
    12  	"github.com/terramate-io/tf/lang/marks"
    13  	"github.com/terramate-io/tf/states"
    14  	"github.com/terramate-io/tf/terminal"
    15  	"github.com/zclconf/go-cty/cty"
    16  )
    17  
    18  // This test is mostly because I am paranoid about having two consecutive
    19  // boolean arguments.
    20  func TestApply_new(t *testing.T) {
    21  	streams, done := terminal.StreamsForTesting(t)
    22  	defer done(t)
    23  	v := NewApply(arguments.ViewHuman, false, NewView(streams).SetRunningInAutomation(true))
    24  	hv, ok := v.(*ApplyHuman)
    25  	if !ok {
    26  		t.Fatalf("unexpected return type %t", v)
    27  	}
    28  
    29  	if hv.destroy != false {
    30  		t.Fatalf("unexpected destroy value")
    31  	}
    32  
    33  	if hv.inAutomation != true {
    34  		t.Fatalf("unexpected inAutomation value")
    35  	}
    36  }
    37  
    38  // Basic test coverage of Outputs, since most of its functionality is tested
    39  // elsewhere.
    40  func TestApplyHuman_outputs(t *testing.T) {
    41  	streams, done := terminal.StreamsForTesting(t)
    42  	v := NewApply(arguments.ViewHuman, false, NewView(streams))
    43  
    44  	v.Outputs(map[string]*states.OutputValue{
    45  		"foo": {Value: cty.StringVal("secret")},
    46  	})
    47  
    48  	got := done(t).Stdout()
    49  	for _, want := range []string{"Outputs:", `foo = "secret"`} {
    50  		if !strings.Contains(got, want) {
    51  			t.Errorf("wrong result\ngot:  %q\nwant: %q", got, want)
    52  		}
    53  	}
    54  }
    55  
    56  // Outputs should do nothing if there are no outputs to render.
    57  func TestApplyHuman_outputsEmpty(t *testing.T) {
    58  	streams, done := terminal.StreamsForTesting(t)
    59  	v := NewApply(arguments.ViewHuman, false, NewView(streams))
    60  
    61  	v.Outputs(map[string]*states.OutputValue{})
    62  
    63  	got := done(t).Stdout()
    64  	if got != "" {
    65  		t.Errorf("output should be empty, but got: %q", got)
    66  	}
    67  }
    68  
    69  // Ensure that the correct view type and in-automation settings propagate to the
    70  // Operation view.
    71  func TestApplyHuman_operation(t *testing.T) {
    72  	streams, done := terminal.StreamsForTesting(t)
    73  	defer done(t)
    74  	v := NewApply(arguments.ViewHuman, false, NewView(streams).SetRunningInAutomation(true)).Operation()
    75  	if hv, ok := v.(*OperationHuman); !ok {
    76  		t.Fatalf("unexpected return type %t", v)
    77  	} else if hv.inAutomation != true {
    78  		t.Fatalf("unexpected inAutomation value on Operation view")
    79  	}
    80  }
    81  
    82  // This view is used for both apply and destroy commands, so the help output
    83  // needs to cover both.
    84  func TestApplyHuman_help(t *testing.T) {
    85  	testCases := map[string]bool{
    86  		"apply":   false,
    87  		"destroy": true,
    88  	}
    89  
    90  	for name, destroy := range testCases {
    91  		t.Run(name, func(t *testing.T) {
    92  			streams, done := terminal.StreamsForTesting(t)
    93  			v := NewApply(arguments.ViewHuman, destroy, NewView(streams))
    94  			v.HelpPrompt()
    95  			got := done(t).Stderr()
    96  			if !strings.Contains(got, name) {
    97  				t.Errorf("wrong result\ngot:  %q\nwant: %q", got, name)
    98  			}
    99  		})
   100  	}
   101  }
   102  
   103  // Hooks and ResourceCount are tangled up and easiest to test together.
   104  func TestApply_resourceCount(t *testing.T) {
   105  	testCases := map[string]struct {
   106  		destroy   bool
   107  		want      string
   108  		importing bool
   109  	}{
   110  		"apply": {
   111  			false,
   112  			"Apply complete! Resources: 1 added, 2 changed, 3 destroyed.",
   113  			false,
   114  		},
   115  		"destroy": {
   116  			true,
   117  			"Destroy complete! Resources: 3 destroyed.",
   118  			false,
   119  		},
   120  		"import": {
   121  			false,
   122  			"Apply complete! Resources: 1 imported, 1 added, 2 changed, 3 destroyed.",
   123  			true,
   124  		},
   125  	}
   126  
   127  	// For compatibility reasons, these tests should hold true for both human
   128  	// and JSON output modes
   129  	views := []arguments.ViewType{arguments.ViewHuman, arguments.ViewJSON}
   130  
   131  	for name, tc := range testCases {
   132  		for _, viewType := range views {
   133  			t.Run(fmt.Sprintf("%s (%s view)", name, viewType), func(t *testing.T) {
   134  				streams, done := terminal.StreamsForTesting(t)
   135  				v := NewApply(viewType, tc.destroy, NewView(streams))
   136  				hooks := v.Hooks()
   137  
   138  				var count *countHook
   139  				for _, hook := range hooks {
   140  					if ch, ok := hook.(*countHook); ok {
   141  						count = ch
   142  					}
   143  				}
   144  				if count == nil {
   145  					t.Fatalf("expected Hooks to include a countHook: %#v", hooks)
   146  				}
   147  
   148  				count.Added = 1
   149  				count.Changed = 2
   150  				count.Removed = 3
   151  
   152  				if tc.importing {
   153  					count.Imported = 1
   154  				}
   155  
   156  				v.ResourceCount("")
   157  
   158  				got := done(t).Stdout()
   159  				if !strings.Contains(got, tc.want) {
   160  					t.Errorf("wrong result\ngot:  %q\nwant: %q", got, tc.want)
   161  				}
   162  			})
   163  		}
   164  	}
   165  }
   166  
   167  func TestApplyHuman_resourceCountStatePath(t *testing.T) {
   168  	testCases := map[string]struct {
   169  		added        int
   170  		changed      int
   171  		removed      int
   172  		statePath    string
   173  		wantContains bool
   174  	}{
   175  		"default state path": {
   176  			added:        1,
   177  			changed:      2,
   178  			removed:      3,
   179  			statePath:    "",
   180  			wantContains: false,
   181  		},
   182  		"only removed": {
   183  			added:        0,
   184  			changed:      0,
   185  			removed:      5,
   186  			statePath:    "foo.tfstate",
   187  			wantContains: false,
   188  		},
   189  		"added": {
   190  			added:        5,
   191  			changed:      0,
   192  			removed:      0,
   193  			statePath:    "foo.tfstate",
   194  			wantContains: true,
   195  		},
   196  		"changed": {
   197  			added:        0,
   198  			changed:      5,
   199  			removed:      0,
   200  			statePath:    "foo.tfstate",
   201  			wantContains: true,
   202  		},
   203  	}
   204  
   205  	for name, tc := range testCases {
   206  		t.Run(name, func(t *testing.T) {
   207  			streams, done := terminal.StreamsForTesting(t)
   208  			v := NewApply(arguments.ViewHuman, false, NewView(streams))
   209  			hooks := v.Hooks()
   210  
   211  			var count *countHook
   212  			for _, hook := range hooks {
   213  				if ch, ok := hook.(*countHook); ok {
   214  					count = ch
   215  				}
   216  			}
   217  			if count == nil {
   218  				t.Fatalf("expected Hooks to include a countHook: %#v", hooks)
   219  			}
   220  
   221  			count.Added = tc.added
   222  			count.Changed = tc.changed
   223  			count.Removed = tc.removed
   224  
   225  			v.ResourceCount(tc.statePath)
   226  
   227  			got := done(t).Stdout()
   228  			want := "State path: " + tc.statePath
   229  			contains := strings.Contains(got, want)
   230  			if contains && !tc.wantContains {
   231  				t.Errorf("wrong result\ngot:  %q\nshould not contain: %q", got, want)
   232  			} else if !contains && tc.wantContains {
   233  				t.Errorf("wrong result\ngot:  %q\nshould contain: %q", got, want)
   234  			}
   235  		})
   236  	}
   237  }
   238  
   239  // Basic test coverage of Outputs, since most of its functionality is tested
   240  // elsewhere.
   241  func TestApplyJSON_outputs(t *testing.T) {
   242  	streams, done := terminal.StreamsForTesting(t)
   243  	v := NewApply(arguments.ViewJSON, false, NewView(streams))
   244  
   245  	v.Outputs(map[string]*states.OutputValue{
   246  		"boop_count": {Value: cty.NumberIntVal(92)},
   247  		"password":   {Value: cty.StringVal("horse-battery").Mark(marks.Sensitive), Sensitive: true},
   248  	})
   249  
   250  	want := []map[string]interface{}{
   251  		{
   252  			"@level":   "info",
   253  			"@message": "Outputs: 2",
   254  			"@module":  "terraform.ui",
   255  			"type":     "outputs",
   256  			"outputs": map[string]interface{}{
   257  				"boop_count": map[string]interface{}{
   258  					"sensitive": false,
   259  					"value":     float64(92),
   260  					"type":      "number",
   261  				},
   262  				"password": map[string]interface{}{
   263  					"sensitive": true,
   264  					"type":      "string",
   265  				},
   266  			},
   267  		},
   268  	}
   269  	testJSONViewOutputEquals(t, done(t).Stdout(), want)
   270  }