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