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  }