github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/command/views/json_view_test.go (about) 1 package views 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "strings" 7 "testing" 8 "time" 9 10 "github.com/google/go-cmp/cmp" 11 "github.com/hashicorp/terraform/internal/addrs" 12 viewsjson "github.com/hashicorp/terraform/internal/command/views/json" 13 "github.com/hashicorp/terraform/internal/plans" 14 "github.com/hashicorp/terraform/internal/terminal" 15 "github.com/hashicorp/terraform/internal/tfdiags" 16 tfversion "github.com/hashicorp/terraform/version" 17 ) 18 19 // Calling NewJSONView should also always output a version message, which is a 20 // convenient way to test that NewJSONView works. 21 func TestNewJSONView(t *testing.T) { 22 streams, done := terminal.StreamsForTesting(t) 23 NewJSONView(NewView(streams)) 24 25 version := tfversion.String() 26 want := []map[string]interface{}{ 27 { 28 "@level": "info", 29 "@message": fmt.Sprintf("Terraform %s", version), 30 "@module": "terraform.ui", 31 "type": "version", 32 "terraform": version, 33 "ui": JSON_UI_VERSION, 34 }, 35 } 36 37 testJSONViewOutputEqualsFull(t, done(t).Stdout(), want) 38 } 39 40 func TestJSONView_Log(t *testing.T) { 41 streams, done := terminal.StreamsForTesting(t) 42 jv := NewJSONView(NewView(streams)) 43 44 jv.Log("hello, world") 45 46 want := []map[string]interface{}{ 47 { 48 "@level": "info", 49 "@message": "hello, world", 50 "@module": "terraform.ui", 51 "type": "log", 52 }, 53 } 54 testJSONViewOutputEquals(t, done(t).Stdout(), want) 55 } 56 57 // This test covers only the basics of JSON diagnostic rendering, as more 58 // complex diagnostics are tested elsewhere. 59 func TestJSONView_Diagnostics(t *testing.T) { 60 streams, done := terminal.StreamsForTesting(t) 61 jv := NewJSONView(NewView(streams)) 62 63 var diags tfdiags.Diagnostics 64 diags = diags.Append(tfdiags.Sourceless( 65 tfdiags.Warning, 66 `Improper use of "less"`, 67 `You probably mean "10 buckets or fewer"`, 68 )) 69 diags = diags.Append(tfdiags.Sourceless( 70 tfdiags.Error, 71 "Unusually stripey cat detected", 72 "Are you sure this random_pet isn't a cheetah?", 73 )) 74 75 jv.Diagnostics(diags) 76 77 want := []map[string]interface{}{ 78 { 79 "@level": "warn", 80 "@message": `Warning: Improper use of "less"`, 81 "@module": "terraform.ui", 82 "type": "diagnostic", 83 "diagnostic": map[string]interface{}{ 84 "severity": "warning", 85 "summary": `Improper use of "less"`, 86 "detail": `You probably mean "10 buckets or fewer"`, 87 }, 88 }, 89 { 90 "@level": "error", 91 "@message": "Error: Unusually stripey cat detected", 92 "@module": "terraform.ui", 93 "type": "diagnostic", 94 "diagnostic": map[string]interface{}{ 95 "severity": "error", 96 "summary": "Unusually stripey cat detected", 97 "detail": "Are you sure this random_pet isn't a cheetah?", 98 }, 99 }, 100 } 101 testJSONViewOutputEquals(t, done(t).Stdout(), want) 102 } 103 104 func TestJSONView_PlannedChange(t *testing.T) { 105 streams, done := terminal.StreamsForTesting(t) 106 jv := NewJSONView(NewView(streams)) 107 108 foo, diags := addrs.ParseModuleInstanceStr("module.foo") 109 if len(diags) > 0 { 110 t.Fatal(diags.Err()) 111 } 112 managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"} 113 cs := &plans.ResourceInstanceChangeSrc{ 114 Addr: managed.Instance(addrs.StringKey("boop")).Absolute(foo), 115 ChangeSrc: plans.ChangeSrc{ 116 Action: plans.Create, 117 }, 118 } 119 jv.PlannedChange(viewsjson.NewResourceInstanceChange(cs)) 120 121 want := []map[string]interface{}{ 122 { 123 "@level": "info", 124 "@message": `module.foo.test_instance.bar["boop"]: Plan to create`, 125 "@module": "terraform.ui", 126 "type": "planned_change", 127 "change": map[string]interface{}{ 128 "action": "create", 129 "resource": map[string]interface{}{ 130 "addr": `module.foo.test_instance.bar["boop"]`, 131 "implied_provider": "test", 132 "module": "module.foo", 133 "resource": `test_instance.bar["boop"]`, 134 "resource_key": "boop", 135 "resource_name": "bar", 136 "resource_type": "test_instance", 137 }, 138 }, 139 }, 140 } 141 testJSONViewOutputEquals(t, done(t).Stdout(), want) 142 } 143 144 func TestJSONView_ResourceDrift(t *testing.T) { 145 streams, done := terminal.StreamsForTesting(t) 146 jv := NewJSONView(NewView(streams)) 147 148 foo, diags := addrs.ParseModuleInstanceStr("module.foo") 149 if len(diags) > 0 { 150 t.Fatal(diags.Err()) 151 } 152 managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"} 153 cs := &plans.ResourceInstanceChangeSrc{ 154 Addr: managed.Instance(addrs.StringKey("boop")).Absolute(foo), 155 ChangeSrc: plans.ChangeSrc{ 156 Action: plans.Update, 157 }, 158 } 159 jv.ResourceDrift(viewsjson.NewResourceInstanceChange(cs)) 160 161 want := []map[string]interface{}{ 162 { 163 "@level": "info", 164 "@message": `module.foo.test_instance.bar["boop"]: Drift detected (update)`, 165 "@module": "terraform.ui", 166 "type": "resource_drift", 167 "change": map[string]interface{}{ 168 "action": "update", 169 "resource": map[string]interface{}{ 170 "addr": `module.foo.test_instance.bar["boop"]`, 171 "implied_provider": "test", 172 "module": "module.foo", 173 "resource": `test_instance.bar["boop"]`, 174 "resource_key": "boop", 175 "resource_name": "bar", 176 "resource_type": "test_instance", 177 }, 178 }, 179 }, 180 } 181 testJSONViewOutputEquals(t, done(t).Stdout(), want) 182 } 183 184 func TestJSONView_ChangeSummary(t *testing.T) { 185 streams, done := terminal.StreamsForTesting(t) 186 jv := NewJSONView(NewView(streams)) 187 188 jv.ChangeSummary(&viewsjson.ChangeSummary{ 189 Add: 1, 190 Change: 2, 191 Remove: 3, 192 Operation: viewsjson.OperationApplied, 193 }) 194 195 want := []map[string]interface{}{ 196 { 197 "@level": "info", 198 "@message": "Apply complete! Resources: 1 added, 2 changed, 3 destroyed.", 199 "@module": "terraform.ui", 200 "type": "change_summary", 201 "changes": map[string]interface{}{ 202 "add": float64(1), 203 "change": float64(2), 204 "remove": float64(3), 205 "operation": "apply", 206 }, 207 }, 208 } 209 testJSONViewOutputEquals(t, done(t).Stdout(), want) 210 } 211 212 func TestJSONView_Hook(t *testing.T) { 213 streams, done := terminal.StreamsForTesting(t) 214 jv := NewJSONView(NewView(streams)) 215 216 foo, diags := addrs.ParseModuleInstanceStr("module.foo") 217 if len(diags) > 0 { 218 t.Fatal(diags.Err()) 219 } 220 managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"} 221 addr := managed.Instance(addrs.StringKey("boop")).Absolute(foo) 222 hook := viewsjson.NewApplyComplete(addr, plans.Create, "id", "boop-beep", 34*time.Second) 223 224 jv.Hook(hook) 225 226 want := []map[string]interface{}{ 227 { 228 "@level": "info", 229 "@message": `module.foo.test_instance.bar["boop"]: Creation complete after 34s [id=boop-beep]`, 230 "@module": "terraform.ui", 231 "type": "apply_complete", 232 "hook": map[string]interface{}{ 233 "resource": map[string]interface{}{ 234 "addr": `module.foo.test_instance.bar["boop"]`, 235 "implied_provider": "test", 236 "module": "module.foo", 237 "resource": `test_instance.bar["boop"]`, 238 "resource_key": "boop", 239 "resource_name": "bar", 240 "resource_type": "test_instance", 241 }, 242 "action": "create", 243 "id_key": "id", 244 "id_value": "boop-beep", 245 "elapsed_seconds": float64(34), 246 }, 247 }, 248 } 249 testJSONViewOutputEquals(t, done(t).Stdout(), want) 250 } 251 252 func TestJSONView_Outputs(t *testing.T) { 253 streams, done := terminal.StreamsForTesting(t) 254 jv := NewJSONView(NewView(streams)) 255 256 jv.Outputs(viewsjson.Outputs{ 257 "boop_count": { 258 Sensitive: false, 259 Value: json.RawMessage(`92`), 260 Type: json.RawMessage(`"number"`), 261 }, 262 "password": { 263 Sensitive: true, 264 Value: json.RawMessage(`"horse-battery"`), 265 Type: json.RawMessage(`"string"`), 266 }, 267 }) 268 269 want := []map[string]interface{}{ 270 { 271 "@level": "info", 272 "@message": "Outputs: 2", 273 "@module": "terraform.ui", 274 "type": "outputs", 275 "outputs": map[string]interface{}{ 276 "boop_count": map[string]interface{}{ 277 "sensitive": false, 278 "value": float64(92), 279 "type": "number", 280 }, 281 "password": map[string]interface{}{ 282 "sensitive": true, 283 "value": "horse-battery", 284 "type": "string", 285 }, 286 }, 287 }, 288 } 289 testJSONViewOutputEquals(t, done(t).Stdout(), want) 290 } 291 292 // This helper function tests a possibly multi-line JSONView output string 293 // against a slice of structs representing the desired log messages. It 294 // verifies that the output of JSONView is in JSON log format, one message per 295 // line. 296 func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string]interface{}) { 297 t.Helper() 298 299 // Remove final trailing newline 300 output = strings.TrimSuffix(output, "\n") 301 302 // Split log into lines, each of which should be a JSON log message 303 gotLines := strings.Split(output, "\n") 304 305 if len(gotLines) != len(want) { 306 t.Errorf("unexpected number of messages. got %d, want %d", len(gotLines), len(want)) 307 } 308 309 // Unmarshal each line and compare to the expected value 310 for i := range gotLines { 311 var gotStruct map[string]interface{} 312 if i >= len(want) { 313 t.Error("reached end of want messages too soon") 314 break 315 } 316 wantStruct := want[i] 317 318 if err := json.Unmarshal([]byte(gotLines[i]), &gotStruct); err != nil { 319 t.Fatal(err) 320 } 321 322 if timestamp, ok := gotStruct["@timestamp"]; !ok { 323 t.Errorf("message has no timestamp: %#v", gotStruct) 324 } else { 325 // Remove the timestamp value from the struct to allow comparison 326 delete(gotStruct, "@timestamp") 327 328 // Verify the timestamp format 329 if _, err := time.Parse("2006-01-02T15:04:05.000000Z07:00", timestamp.(string)); err != nil { 330 t.Errorf("error parsing timestamp on line %d: %s", i, err) 331 } 332 } 333 334 if !cmp.Equal(wantStruct, gotStruct) { 335 t.Errorf("unexpected output on line %d:\n%s", i, cmp.Diff(wantStruct, gotStruct)) 336 } 337 } 338 } 339 340 // testJSONViewOutputEquals skips the first line of output, since it ought to 341 // be a version message that we don't care about for most of our tests. 342 func testJSONViewOutputEquals(t *testing.T, output string, want []map[string]interface{}) { 343 t.Helper() 344 345 // Remove up to the first newline 346 index := strings.Index(output, "\n") 347 if index >= 0 { 348 output = output[index+1:] 349 } 350 testJSONViewOutputEqualsFull(t, output, want) 351 }