github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/repl/session_test.go (about) 1 package repl 2 3 import ( 4 "flag" 5 "os" 6 "strings" 7 "testing" 8 9 "github.com/zclconf/go-cty/cty" 10 11 "github.com/hashicorp/terraform/internal/addrs" 12 "github.com/hashicorp/terraform/internal/configs/configschema" 13 "github.com/hashicorp/terraform/internal/initwd" 14 "github.com/hashicorp/terraform/internal/providers" 15 "github.com/hashicorp/terraform/internal/states" 16 "github.com/hashicorp/terraform/internal/terraform" 17 18 _ "github.com/hashicorp/terraform/internal/logging" 19 ) 20 21 func TestMain(m *testing.M) { 22 flag.Parse() 23 os.Exit(m.Run()) 24 } 25 26 func TestSession_basicState(t *testing.T) { 27 state := states.BuildState(func(s *states.SyncState) { 28 s.SetResourceInstanceCurrent( 29 addrs.Resource{ 30 Mode: addrs.ManagedResourceMode, 31 Type: "test_instance", 32 Name: "foo", 33 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 34 &states.ResourceInstanceObjectSrc{ 35 Status: states.ObjectReady, 36 AttrsJSON: []byte(`{"id":"bar"}`), 37 }, 38 addrs.AbsProviderConfig{ 39 Provider: addrs.NewDefaultProvider("test"), 40 Module: addrs.RootModule, 41 }, 42 ) 43 s.SetResourceInstanceCurrent( 44 addrs.Resource{ 45 Mode: addrs.ManagedResourceMode, 46 Type: "test_instance", 47 Name: "foo", 48 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance.Child("module", addrs.NoKey)), 49 &states.ResourceInstanceObjectSrc{ 50 Status: states.ObjectReady, 51 AttrsJSON: []byte(`{"id":"bar"}`), 52 }, 53 addrs.AbsProviderConfig{ 54 Provider: addrs.NewDefaultProvider("test"), 55 Module: addrs.RootModule, 56 }, 57 ) 58 }) 59 60 t.Run("basic", func(t *testing.T) { 61 testSession(t, testSessionTest{ 62 State: state, 63 Inputs: []testSessionInput{ 64 { 65 Input: "test_instance.foo.id", 66 Output: `"bar"`, 67 }, 68 }, 69 }) 70 }) 71 72 t.Run("missing resource", func(t *testing.T) { 73 testSession(t, testSessionTest{ 74 State: state, 75 Inputs: []testSessionInput{ 76 { 77 Input: "test_instance.bar.id", 78 Error: true, 79 ErrorContains: `A managed resource "test_instance" "bar" has not been declared`, 80 }, 81 }, 82 }) 83 }) 84 85 t.Run("missing module", func(t *testing.T) { 86 testSession(t, testSessionTest{ 87 State: state, 88 Inputs: []testSessionInput{ 89 { 90 Input: "module.child", 91 Error: true, 92 ErrorContains: `No module call named "child" is declared in the root module.`, 93 }, 94 }, 95 }) 96 }) 97 98 t.Run("missing module referencing just one output", func(t *testing.T) { 99 testSession(t, testSessionTest{ 100 State: state, 101 Inputs: []testSessionInput{ 102 { 103 Input: "module.child.foo", 104 Error: true, 105 ErrorContains: `No module call named "child" is declared in the root module.`, 106 }, 107 }, 108 }) 109 }) 110 111 t.Run("missing module output", func(t *testing.T) { 112 testSession(t, testSessionTest{ 113 State: state, 114 Inputs: []testSessionInput{ 115 { 116 Input: "module.module.foo", 117 Error: true, 118 ErrorContains: `Unsupported attribute: This object does not have an attribute named "foo"`, 119 }, 120 }, 121 }) 122 }) 123 } 124 125 func TestSession_stateless(t *testing.T) { 126 t.Run("exit", func(t *testing.T) { 127 testSession(t, testSessionTest{ 128 Inputs: []testSessionInput{ 129 { 130 Input: "exit", 131 Exit: true, 132 }, 133 }, 134 }) 135 }) 136 137 t.Run("help", func(t *testing.T) { 138 testSession(t, testSessionTest{ 139 Inputs: []testSessionInput{ 140 { 141 Input: "help", 142 OutputContains: "allows you to", 143 }, 144 }, 145 }) 146 }) 147 148 t.Run("help with spaces", func(t *testing.T) { 149 testSession(t, testSessionTest{ 150 Inputs: []testSessionInput{ 151 { 152 Input: "help ", 153 OutputContains: "allows you to", 154 }, 155 }, 156 }) 157 }) 158 159 t.Run("basic math", func(t *testing.T) { 160 testSession(t, testSessionTest{ 161 Inputs: []testSessionInput{ 162 { 163 Input: "1 + 5", 164 Output: "6", 165 }, 166 }, 167 }) 168 }) 169 170 t.Run("missing resource", func(t *testing.T) { 171 testSession(t, testSessionTest{ 172 Inputs: []testSessionInput{ 173 { 174 Input: "test_instance.bar.id", 175 Error: true, 176 ErrorContains: `resource "test_instance" "bar" has not been declared`, 177 }, 178 }, 179 }) 180 }) 181 } 182 183 func testSession(t *testing.T, test testSessionTest) { 184 t.Helper() 185 186 p := &terraform.MockProvider{} 187 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 188 ResourceTypes: map[string]providers.Schema{ 189 "test_instance": { 190 Block: &configschema.Block{ 191 Attributes: map[string]*configschema.Attribute{ 192 "id": {Type: cty.String, Computed: true}, 193 }, 194 }, 195 }, 196 }, 197 } 198 199 config, _, cleanup, configDiags := initwd.LoadConfigForTests(t, "testdata/config-fixture") 200 defer cleanup() 201 if configDiags.HasErrors() { 202 t.Fatalf("unexpected problems loading config: %s", configDiags.Err()) 203 } 204 205 // Build the TF context 206 ctx, diags := terraform.NewContext(&terraform.ContextOpts{ 207 Providers: map[addrs.Provider]providers.Factory{ 208 addrs.NewDefaultProvider("test"): providers.FactoryFixed(p), 209 }, 210 }) 211 if diags.HasErrors() { 212 t.Fatalf("failed to create context: %s", diags.Err()) 213 } 214 215 state := test.State 216 if state == nil { 217 state = states.NewState() 218 } 219 scope, diags := ctx.Eval(config, state, addrs.RootModuleInstance, &terraform.EvalOpts{}) 220 if diags.HasErrors() { 221 t.Fatalf("failed to create scope: %s", diags.Err()) 222 } 223 224 // Build the session 225 s := &Session{ 226 Scope: scope, 227 } 228 229 // Test the inputs. We purposely don't use subtests here because 230 // the inputs don't represent subtests, but a sequence of stateful 231 // operations. 232 for _, input := range test.Inputs { 233 result, exit, diags := s.Handle(input.Input) 234 if exit != input.Exit { 235 t.Fatalf("incorrect 'exit' result %t; want %t", exit, input.Exit) 236 } 237 if (diags.HasErrors()) != input.Error { 238 t.Fatalf("%q: unexpected errors: %s", input.Input, diags.Err()) 239 } 240 if diags.HasErrors() { 241 if input.ErrorContains != "" { 242 if !strings.Contains(diags.Err().Error(), input.ErrorContains) { 243 t.Fatalf( 244 "%q: diagnostics should contain: %q\n\n%s", 245 input.Input, input.ErrorContains, diags.Err(), 246 ) 247 } 248 } 249 250 continue 251 } 252 253 if input.Output != "" && result != input.Output { 254 t.Fatalf( 255 "%q: expected:\n\n%s\n\ngot:\n\n%s", 256 input.Input, input.Output, result) 257 } 258 259 if input.OutputContains != "" && !strings.Contains(result, input.OutputContains) { 260 t.Fatalf( 261 "%q: expected contains:\n\n%s\n\ngot:\n\n%s", 262 input.Input, input.OutputContains, result) 263 } 264 } 265 } 266 267 type testSessionTest struct { 268 State *states.State // State to use 269 Module string // Module name in testdata to load 270 271 // Inputs are the list of test inputs that are run in order. 272 // Each input can test the output of each step. 273 Inputs []testSessionInput 274 } 275 276 // testSessionInput is a single input to test for a session. 277 type testSessionInput struct { 278 Input string // Input string 279 Output string // Exact output string to check 280 OutputContains string 281 Error bool // Error is true if error is expected 282 Exit bool // Exit is true if exiting is expected 283 ErrorContains string 284 }