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  }