github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/repl/session_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package repl 5 6 import ( 7 "flag" 8 "os" 9 "strings" 10 "testing" 11 12 "github.com/google/go-cmp/cmp" 13 "github.com/zclconf/go-cty/cty" 14 15 "github.com/terramate-io/tf/addrs" 16 "github.com/terramate-io/tf/configs/configschema" 17 "github.com/terramate-io/tf/initwd" 18 "github.com/terramate-io/tf/providers" 19 "github.com/terramate-io/tf/states" 20 "github.com/terramate-io/tf/terraform" 21 22 _ "github.com/terramate-io/tf/logging" 23 ) 24 25 func TestMain(m *testing.M) { 26 flag.Parse() 27 os.Exit(m.Run()) 28 } 29 30 func TestSession_basicState(t *testing.T) { 31 state := states.BuildState(func(s *states.SyncState) { 32 s.SetResourceInstanceCurrent( 33 addrs.Resource{ 34 Mode: addrs.ManagedResourceMode, 35 Type: "test_instance", 36 Name: "foo", 37 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 38 &states.ResourceInstanceObjectSrc{ 39 Status: states.ObjectReady, 40 AttrsJSON: []byte(`{"id":"bar"}`), 41 }, 42 addrs.AbsProviderConfig{ 43 Provider: addrs.NewDefaultProvider("test"), 44 Module: addrs.RootModule, 45 }, 46 ) 47 s.SetResourceInstanceCurrent( 48 addrs.Resource{ 49 Mode: addrs.ManagedResourceMode, 50 Type: "test_instance", 51 Name: "foo", 52 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance.Child("module", addrs.NoKey)), 53 &states.ResourceInstanceObjectSrc{ 54 Status: states.ObjectReady, 55 AttrsJSON: []byte(`{"id":"bar"}`), 56 }, 57 addrs.AbsProviderConfig{ 58 Provider: addrs.NewDefaultProvider("test"), 59 Module: addrs.RootModule, 60 }, 61 ) 62 }) 63 64 t.Run("basic", func(t *testing.T) { 65 testSession(t, testSessionTest{ 66 State: state, 67 Inputs: []testSessionInput{ 68 { 69 Input: "test_instance.foo.id", 70 Output: `"bar"`, 71 }, 72 }, 73 }) 74 }) 75 76 t.Run("missing resource", func(t *testing.T) { 77 testSession(t, testSessionTest{ 78 State: state, 79 Inputs: []testSessionInput{ 80 { 81 Input: "test_instance.bar.id", 82 Error: true, 83 ErrorContains: `A managed resource "test_instance" "bar" has not been declared`, 84 }, 85 }, 86 }) 87 }) 88 89 t.Run("missing module", func(t *testing.T) { 90 testSession(t, testSessionTest{ 91 State: state, 92 Inputs: []testSessionInput{ 93 { 94 Input: "module.child", 95 Error: true, 96 ErrorContains: `No module call named "child" is declared in the root module.`, 97 }, 98 }, 99 }) 100 }) 101 102 t.Run("missing module referencing just one output", func(t *testing.T) { 103 testSession(t, testSessionTest{ 104 State: state, 105 Inputs: []testSessionInput{ 106 { 107 Input: "module.child.foo", 108 Error: true, 109 ErrorContains: `No module call named "child" is declared in the root module.`, 110 }, 111 }, 112 }) 113 }) 114 115 t.Run("missing module output", func(t *testing.T) { 116 testSession(t, testSessionTest{ 117 State: state, 118 Inputs: []testSessionInput{ 119 { 120 Input: "module.module.foo", 121 Error: true, 122 ErrorContains: `Unsupported attribute: This object does not have an attribute named "foo"`, 123 }, 124 }, 125 }) 126 }) 127 128 t.Run("type function", func(t *testing.T) { 129 testSession(t, testSessionTest{ 130 State: state, 131 Inputs: []testSessionInput{ 132 { 133 Input: "type(test_instance.foo)", 134 Output: `object({ 135 id: string, 136 })`, 137 }, 138 }, 139 }) 140 }) 141 } 142 143 func TestSession_stateless(t *testing.T) { 144 t.Run("exit", func(t *testing.T) { 145 testSession(t, testSessionTest{ 146 Inputs: []testSessionInput{ 147 { 148 Input: "exit", 149 Exit: true, 150 }, 151 }, 152 }) 153 }) 154 155 t.Run("help", func(t *testing.T) { 156 testSession(t, testSessionTest{ 157 Inputs: []testSessionInput{ 158 { 159 Input: "help", 160 OutputContains: "allows you to", 161 }, 162 }, 163 }) 164 }) 165 166 t.Run("help with spaces", func(t *testing.T) { 167 testSession(t, testSessionTest{ 168 Inputs: []testSessionInput{ 169 { 170 Input: "help ", 171 OutputContains: "allows you to", 172 }, 173 }, 174 }) 175 }) 176 177 t.Run("basic math", func(t *testing.T) { 178 testSession(t, testSessionTest{ 179 Inputs: []testSessionInput{ 180 { 181 Input: "1 + 5", 182 Output: "6", 183 }, 184 }, 185 }) 186 }) 187 188 t.Run("missing resource", func(t *testing.T) { 189 testSession(t, testSessionTest{ 190 Inputs: []testSessionInput{ 191 { 192 Input: "test_instance.bar.id", 193 Error: true, 194 ErrorContains: `resource "test_instance" "bar" has not been declared`, 195 }, 196 }, 197 }) 198 }) 199 200 t.Run("type function", func(t *testing.T) { 201 testSession(t, testSessionTest{ 202 Inputs: []testSessionInput{ 203 { 204 Input: `type("foo")`, 205 Output: "string", 206 }, 207 }, 208 }) 209 }) 210 211 t.Run("type type is type", func(t *testing.T) { 212 testSession(t, testSessionTest{ 213 Inputs: []testSessionInput{ 214 { 215 Input: `type(type("foo"))`, 216 Output: "type", 217 }, 218 }, 219 }) 220 }) 221 222 t.Run("interpolating type with strings is not possible", func(t *testing.T) { 223 testSession(t, testSessionTest{ 224 Inputs: []testSessionInput{ 225 { 226 Input: `"quin${type([])}"`, 227 Error: true, 228 ErrorContains: "Invalid template interpolation value", 229 }, 230 }, 231 }) 232 }) 233 234 t.Run("type function cannot be used in expressions", func(t *testing.T) { 235 testSession(t, testSessionTest{ 236 Inputs: []testSessionInput{ 237 { 238 Input: `[for i in [1, "two", true]: type(i)]`, 239 Output: "", 240 Error: true, 241 ErrorContains: "Invalid use of type function", 242 }, 243 }, 244 }) 245 }) 246 247 t.Run("type equality checks are not permitted", func(t *testing.T) { 248 testSession(t, testSessionTest{ 249 Inputs: []testSessionInput{ 250 { 251 Input: `type("foo") == type("bar")`, 252 Output: "", 253 Error: true, 254 ErrorContains: "Invalid use of type function", 255 }, 256 }, 257 }) 258 }) 259 } 260 261 func testSession(t *testing.T, test testSessionTest) { 262 t.Helper() 263 264 p := &terraform.MockProvider{} 265 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 266 ResourceTypes: map[string]providers.Schema{ 267 "test_instance": { 268 Block: &configschema.Block{ 269 Attributes: map[string]*configschema.Attribute{ 270 "id": {Type: cty.String, Computed: true}, 271 }, 272 }, 273 }, 274 }, 275 } 276 277 config, _, cleanup, configDiags := initwd.LoadConfigForTests(t, "testdata/config-fixture", "tests") 278 defer cleanup() 279 if configDiags.HasErrors() { 280 t.Fatalf("unexpected problems loading config: %s", configDiags.Err()) 281 } 282 283 // Build the TF context 284 ctx, diags := terraform.NewContext(&terraform.ContextOpts{ 285 Providers: map[addrs.Provider]providers.Factory{ 286 addrs.NewDefaultProvider("test"): providers.FactoryFixed(p), 287 }, 288 }) 289 if diags.HasErrors() { 290 t.Fatalf("failed to create context: %s", diags.Err()) 291 } 292 293 state := test.State 294 if state == nil { 295 state = states.NewState() 296 } 297 scope, diags := ctx.Eval(config, state, addrs.RootModuleInstance, &terraform.EvalOpts{}) 298 if diags.HasErrors() { 299 t.Fatalf("failed to create scope: %s", diags.Err()) 300 } 301 302 // Ensure that any console-only functions are available 303 scope.ConsoleMode = true 304 305 // Build the session 306 s := &Session{ 307 Scope: scope, 308 } 309 310 // Test the inputs. We purposely don't use subtests here because 311 // the inputs don't represent subtests, but a sequence of stateful 312 // operations. 313 for _, input := range test.Inputs { 314 result, exit, diags := s.Handle(input.Input) 315 if exit != input.Exit { 316 t.Fatalf("incorrect 'exit' result %t; want %t", exit, input.Exit) 317 } 318 if (diags.HasErrors()) != input.Error { 319 t.Fatalf("%q: unexpected errors: %s", input.Input, diags.Err()) 320 } 321 if diags.HasErrors() { 322 if input.ErrorContains != "" { 323 if !strings.Contains(diags.Err().Error(), input.ErrorContains) { 324 t.Fatalf( 325 "%q: diagnostics should contain: %q\n\n%s", 326 input.Input, input.ErrorContains, diags.Err(), 327 ) 328 } 329 } 330 331 continue 332 } 333 334 if input.Output != "" && result != input.Output { 335 t.Fatalf( 336 "%q: expected:\n\n%s\n\ngot:\n\n%s", 337 input.Input, input.Output, result) 338 } 339 340 if input.OutputContains != "" && !strings.Contains(result, input.OutputContains) { 341 t.Fatalf( 342 "%q: expected contains:\n\n%s\n\ngot:\n\n%s", 343 input.Input, input.OutputContains, result) 344 } 345 } 346 } 347 348 type testSessionTest struct { 349 State *states.State // State to use 350 Module string // Module name in testdata to load 351 352 // Inputs are the list of test inputs that are run in order. 353 // Each input can test the output of each step. 354 Inputs []testSessionInput 355 } 356 357 // testSessionInput is a single input to test for a session. 358 type testSessionInput struct { 359 Input string // Input string 360 Output string // Exact output string to check 361 OutputContains string 362 Error bool // Error is true if error is expected 363 Exit bool // Exit is true if exiting is expected 364 ErrorContains string 365 } 366 367 func TestTypeString(t *testing.T) { 368 tests := []struct { 369 Input cty.Value 370 Want string 371 }{ 372 // Primititves 373 { 374 cty.StringVal("a"), 375 "string", 376 }, 377 { 378 cty.NumberIntVal(42), 379 "number", 380 }, 381 { 382 cty.BoolVal(true), 383 "bool", 384 }, 385 // Collections 386 { 387 cty.EmptyObjectVal, 388 `object({})`, 389 }, 390 { 391 cty.EmptyTupleVal, 392 `tuple([])`, 393 }, 394 { 395 cty.ListValEmpty(cty.String), 396 `list(string)`, 397 }, 398 { 399 cty.MapValEmpty(cty.String), 400 `map(string)`, 401 }, 402 { 403 cty.SetValEmpty(cty.String), 404 `set(string)`, 405 }, 406 { 407 cty.ListVal([]cty.Value{cty.StringVal("a")}), 408 `list(string)`, 409 }, 410 { 411 cty.ListVal([]cty.Value{cty.ListVal([]cty.Value{cty.NumberIntVal(42)})}), 412 `list(list(number))`, 413 }, 414 { 415 cty.ListVal([]cty.Value{cty.MapValEmpty(cty.String)}), 416 `list(map(string))`, 417 }, 418 { 419 cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ 420 "foo": cty.StringVal("bar"), 421 })}), 422 "list(\n object({\n foo: string,\n }),\n)", 423 }, 424 // Unknowns and Nulls 425 { 426 cty.UnknownVal(cty.String), 427 "string", 428 }, 429 { 430 cty.NullVal(cty.Object(map[string]cty.Type{ 431 "foo": cty.String, 432 })), 433 "object({\n foo: string,\n})", 434 }, 435 { // irrelevant marks do nothing 436 cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ 437 "foo": cty.StringVal("bar").Mark("ignore me"), 438 })}), 439 "list(\n object({\n foo: string,\n }),\n)", 440 }, 441 } 442 for _, test := range tests { 443 got := typeString(test.Input.Type()) 444 if got != test.Want { 445 t.Errorf("wrong result:\n%s", cmp.Diff(got, test.Want)) 446 } 447 } 448 }