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 }