github.com/opentofu/opentofu@v1.7.1/internal/legacy/helper/schema/provider_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package schema
     7  
     8  import (
     9  	"fmt"
    10  	"reflect"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/google/go-cmp/cmp"
    16  	"github.com/zclconf/go-cty/cty"
    17  
    18  	"github.com/opentofu/opentofu/internal/configs/configschema"
    19  	"github.com/opentofu/opentofu/internal/legacy/tofu"
    20  )
    21  
    22  func TestProvider_impl(t *testing.T) {
    23  	var _ tofu.ResourceProvider = new(Provider)
    24  }
    25  
    26  func TestProviderGetSchema(t *testing.T) {
    27  	// This functionality is already broadly tested in core_schema_test.go,
    28  	// so this is just to ensure that the call passes through correctly.
    29  	p := &Provider{
    30  		Schema: map[string]*Schema{
    31  			"bar": {
    32  				Type:     TypeString,
    33  				Required: true,
    34  			},
    35  		},
    36  		ResourcesMap: map[string]*Resource{
    37  			"foo": &Resource{
    38  				Schema: map[string]*Schema{
    39  					"bar": {
    40  						Type:     TypeString,
    41  						Required: true,
    42  					},
    43  				},
    44  			},
    45  		},
    46  		DataSourcesMap: map[string]*Resource{
    47  			"baz": &Resource{
    48  				Schema: map[string]*Schema{
    49  					"bur": {
    50  						Type:     TypeString,
    51  						Required: true,
    52  					},
    53  				},
    54  			},
    55  		},
    56  	}
    57  
    58  	want := &tofu.ProviderSchema{
    59  		Provider: &configschema.Block{
    60  			Attributes: map[string]*configschema.Attribute{
    61  				"bar": &configschema.Attribute{
    62  					Type:     cty.String,
    63  					Required: true,
    64  				},
    65  			},
    66  			BlockTypes: map[string]*configschema.NestedBlock{},
    67  		},
    68  		ResourceTypes: map[string]*configschema.Block{
    69  			"foo": testResource(&configschema.Block{
    70  				Attributes: map[string]*configschema.Attribute{
    71  					"bar": &configschema.Attribute{
    72  						Type:     cty.String,
    73  						Required: true,
    74  					},
    75  				},
    76  				BlockTypes: map[string]*configschema.NestedBlock{},
    77  			}),
    78  		},
    79  		DataSources: map[string]*configschema.Block{
    80  			"baz": testResource(&configschema.Block{
    81  				Attributes: map[string]*configschema.Attribute{
    82  					"bur": &configschema.Attribute{
    83  						Type:     cty.String,
    84  						Required: true,
    85  					},
    86  				},
    87  				BlockTypes: map[string]*configschema.NestedBlock{},
    88  			}),
    89  		},
    90  	}
    91  	got, err := p.GetSchema(&tofu.ProviderSchemaRequest{
    92  		ResourceTypes: []string{"foo", "bar"},
    93  		DataSources:   []string{"baz", "bar"},
    94  	})
    95  	if err != nil {
    96  		t.Fatalf("unexpected error %s", err)
    97  	}
    98  
    99  	if !cmp.Equal(got, want, equateEmpty, typeComparer) {
   100  		t.Error("wrong result:\n", cmp.Diff(got, want, equateEmpty, typeComparer))
   101  	}
   102  }
   103  
   104  func TestProviderConfigure(t *testing.T) {
   105  	cases := []struct {
   106  		P      *Provider
   107  		Config map[string]interface{}
   108  		Err    bool
   109  	}{
   110  		{
   111  			P:      &Provider{},
   112  			Config: nil,
   113  			Err:    false,
   114  		},
   115  
   116  		{
   117  			P: &Provider{
   118  				Schema: map[string]*Schema{
   119  					"foo": &Schema{
   120  						Type:     TypeInt,
   121  						Optional: true,
   122  					},
   123  				},
   124  
   125  				ConfigureFunc: func(d *ResourceData) (interface{}, error) {
   126  					if d.Get("foo").(int) == 42 {
   127  						return nil, nil
   128  					}
   129  
   130  					return nil, fmt.Errorf("nope")
   131  				},
   132  			},
   133  			Config: map[string]interface{}{
   134  				"foo": 42,
   135  			},
   136  			Err: false,
   137  		},
   138  
   139  		{
   140  			P: &Provider{
   141  				Schema: map[string]*Schema{
   142  					"foo": &Schema{
   143  						Type:     TypeInt,
   144  						Optional: true,
   145  					},
   146  				},
   147  
   148  				ConfigureFunc: func(d *ResourceData) (interface{}, error) {
   149  					if d.Get("foo").(int) == 42 {
   150  						return nil, nil
   151  					}
   152  
   153  					return nil, fmt.Errorf("nope")
   154  				},
   155  			},
   156  			Config: map[string]interface{}{
   157  				"foo": 52,
   158  			},
   159  			Err: true,
   160  		},
   161  	}
   162  
   163  	for i, tc := range cases {
   164  		c := tofu.NewResourceConfigRaw(tc.Config)
   165  		err := tc.P.Configure(c)
   166  		if err != nil != tc.Err {
   167  			t.Fatalf("%d: %s", i, err)
   168  		}
   169  	}
   170  }
   171  
   172  func TestProviderResources(t *testing.T) {
   173  	cases := []struct {
   174  		P      *Provider
   175  		Result []tofu.ResourceType
   176  	}{
   177  		{
   178  			P:      &Provider{},
   179  			Result: []tofu.ResourceType{},
   180  		},
   181  
   182  		{
   183  			P: &Provider{
   184  				ResourcesMap: map[string]*Resource{
   185  					"foo": nil,
   186  					"bar": nil,
   187  				},
   188  			},
   189  			Result: []tofu.ResourceType{
   190  				tofu.ResourceType{Name: "bar", SchemaAvailable: true},
   191  				tofu.ResourceType{Name: "foo", SchemaAvailable: true},
   192  			},
   193  		},
   194  
   195  		{
   196  			P: &Provider{
   197  				ResourcesMap: map[string]*Resource{
   198  					"foo": nil,
   199  					"bar": &Resource{Importer: &ResourceImporter{}},
   200  					"baz": nil,
   201  				},
   202  			},
   203  			Result: []tofu.ResourceType{
   204  				tofu.ResourceType{Name: "bar", Importable: true, SchemaAvailable: true},
   205  				tofu.ResourceType{Name: "baz", SchemaAvailable: true},
   206  				tofu.ResourceType{Name: "foo", SchemaAvailable: true},
   207  			},
   208  		},
   209  	}
   210  
   211  	for i, tc := range cases {
   212  		actual := tc.P.Resources()
   213  		if !reflect.DeepEqual(actual, tc.Result) {
   214  			t.Fatalf("%d: %#v", i, actual)
   215  		}
   216  	}
   217  }
   218  
   219  func TestProviderDataSources(t *testing.T) {
   220  	cases := []struct {
   221  		P      *Provider
   222  		Result []tofu.DataSource
   223  	}{
   224  		{
   225  			P:      &Provider{},
   226  			Result: []tofu.DataSource{},
   227  		},
   228  
   229  		{
   230  			P: &Provider{
   231  				DataSourcesMap: map[string]*Resource{
   232  					"foo": nil,
   233  					"bar": nil,
   234  				},
   235  			},
   236  			Result: []tofu.DataSource{
   237  				tofu.DataSource{Name: "bar", SchemaAvailable: true},
   238  				tofu.DataSource{Name: "foo", SchemaAvailable: true},
   239  			},
   240  		},
   241  	}
   242  
   243  	for i, tc := range cases {
   244  		actual := tc.P.DataSources()
   245  		if !reflect.DeepEqual(actual, tc.Result) {
   246  			t.Fatalf("%d: got %#v; want %#v", i, actual, tc.Result)
   247  		}
   248  	}
   249  }
   250  
   251  func TestProviderValidate(t *testing.T) {
   252  	cases := []struct {
   253  		P      *Provider
   254  		Config map[string]interface{}
   255  		Err    bool
   256  	}{
   257  		{
   258  			P: &Provider{
   259  				Schema: map[string]*Schema{
   260  					"foo": &Schema{},
   261  				},
   262  			},
   263  			Config: nil,
   264  			Err:    true,
   265  		},
   266  	}
   267  
   268  	for i, tc := range cases {
   269  		c := tofu.NewResourceConfigRaw(tc.Config)
   270  		_, es := tc.P.Validate(c)
   271  		if len(es) > 0 != tc.Err {
   272  			t.Fatalf("%d: %#v", i, es)
   273  		}
   274  	}
   275  }
   276  
   277  func TestProviderDiff_legacyTimeoutType(t *testing.T) {
   278  	p := &Provider{
   279  		ResourcesMap: map[string]*Resource{
   280  			"blah": &Resource{
   281  				Schema: map[string]*Schema{
   282  					"foo": {
   283  						Type:     TypeInt,
   284  						Optional: true,
   285  					},
   286  				},
   287  				Timeouts: &ResourceTimeout{
   288  					Create: DefaultTimeout(10 * time.Minute),
   289  				},
   290  			},
   291  		},
   292  	}
   293  
   294  	invalidCfg := map[string]interface{}{
   295  		"foo": 42,
   296  		"timeouts": []interface{}{
   297  			map[string]interface{}{
   298  				"create": "40m",
   299  			},
   300  		},
   301  	}
   302  	ic := tofu.NewResourceConfigRaw(invalidCfg)
   303  	_, err := p.Diff(
   304  		&tofu.InstanceInfo{
   305  			Type: "blah",
   306  		},
   307  		nil,
   308  		ic,
   309  	)
   310  	if err != nil {
   311  		t.Fatal(err)
   312  	}
   313  }
   314  
   315  func TestProviderDiff_timeoutInvalidValue(t *testing.T) {
   316  	p := &Provider{
   317  		ResourcesMap: map[string]*Resource{
   318  			"blah": &Resource{
   319  				Schema: map[string]*Schema{
   320  					"foo": {
   321  						Type:     TypeInt,
   322  						Optional: true,
   323  					},
   324  				},
   325  				Timeouts: &ResourceTimeout{
   326  					Create: DefaultTimeout(10 * time.Minute),
   327  				},
   328  			},
   329  		},
   330  	}
   331  
   332  	invalidCfg := map[string]interface{}{
   333  		"foo": 42,
   334  		"timeouts": map[string]interface{}{
   335  			"create": "invalid",
   336  		},
   337  	}
   338  	ic := tofu.NewResourceConfigRaw(invalidCfg)
   339  	_, err := p.Diff(
   340  		&tofu.InstanceInfo{
   341  			Type: "blah",
   342  		},
   343  		nil,
   344  		ic,
   345  	)
   346  	if err == nil {
   347  		t.Fatal("Expected provider.Diff to fail with invalid timeout value")
   348  	}
   349  	expectedErrMsg := `time: invalid duration "invalid"`
   350  	if !strings.Contains(err.Error(), expectedErrMsg) {
   351  		t.Fatalf("Unexpected error message: %q\nExpected message to contain %q",
   352  			err.Error(),
   353  			expectedErrMsg)
   354  	}
   355  }
   356  
   357  func TestProviderValidateResource(t *testing.T) {
   358  	cases := []struct {
   359  		P      *Provider
   360  		Type   string
   361  		Config map[string]interface{}
   362  		Err    bool
   363  	}{
   364  		{
   365  			P:      &Provider{},
   366  			Type:   "foo",
   367  			Config: nil,
   368  			Err:    true,
   369  		},
   370  
   371  		{
   372  			P: &Provider{
   373  				ResourcesMap: map[string]*Resource{
   374  					"foo": &Resource{},
   375  				},
   376  			},
   377  			Type:   "foo",
   378  			Config: nil,
   379  			Err:    false,
   380  		},
   381  	}
   382  
   383  	for i, tc := range cases {
   384  		c := tofu.NewResourceConfigRaw(tc.Config)
   385  		_, es := tc.P.ValidateResource(tc.Type, c)
   386  		if len(es) > 0 != tc.Err {
   387  			t.Fatalf("%d: %#v", i, es)
   388  		}
   389  	}
   390  }
   391  
   392  func TestProviderImportState_default(t *testing.T) {
   393  	p := &Provider{
   394  		ResourcesMap: map[string]*Resource{
   395  			"foo": &Resource{
   396  				Importer: &ResourceImporter{},
   397  			},
   398  		},
   399  	}
   400  
   401  	states, err := p.ImportState(&tofu.InstanceInfo{
   402  		Type: "foo",
   403  	}, "bar")
   404  	if err != nil {
   405  		t.Fatalf("err: %s", err)
   406  	}
   407  
   408  	if len(states) != 1 {
   409  		t.Fatalf("bad: %#v", states)
   410  	}
   411  	if states[0].ID != "bar" {
   412  		t.Fatalf("bad: %#v", states)
   413  	}
   414  }
   415  
   416  func TestProviderImportState_setsId(t *testing.T) {
   417  	var val string
   418  	stateFunc := func(d *ResourceData, meta interface{}) ([]*ResourceData, error) {
   419  		val = d.Id()
   420  		return []*ResourceData{d}, nil
   421  	}
   422  
   423  	p := &Provider{
   424  		ResourcesMap: map[string]*Resource{
   425  			"foo": &Resource{
   426  				Importer: &ResourceImporter{
   427  					State: stateFunc,
   428  				},
   429  			},
   430  		},
   431  	}
   432  
   433  	_, err := p.ImportState(&tofu.InstanceInfo{
   434  		Type: "foo",
   435  	}, "bar")
   436  	if err != nil {
   437  		t.Fatalf("err: %s", err)
   438  	}
   439  
   440  	if val != "bar" {
   441  		t.Fatal("should set id")
   442  	}
   443  }
   444  
   445  func TestProviderImportState_setsType(t *testing.T) {
   446  	var tVal string
   447  	stateFunc := func(d *ResourceData, meta interface{}) ([]*ResourceData, error) {
   448  		d.SetId("foo")
   449  		tVal = d.State().Ephemeral.Type
   450  		return []*ResourceData{d}, nil
   451  	}
   452  
   453  	p := &Provider{
   454  		ResourcesMap: map[string]*Resource{
   455  			"foo": &Resource{
   456  				Importer: &ResourceImporter{
   457  					State: stateFunc,
   458  				},
   459  			},
   460  		},
   461  	}
   462  
   463  	_, err := p.ImportState(&tofu.InstanceInfo{
   464  		Type: "foo",
   465  	}, "bar")
   466  	if err != nil {
   467  		t.Fatalf("err: %s", err)
   468  	}
   469  
   470  	if tVal != "foo" {
   471  		t.Fatal("should set type")
   472  	}
   473  }
   474  
   475  func TestProviderMeta(t *testing.T) {
   476  	p := new(Provider)
   477  	if v := p.Meta(); v != nil {
   478  		t.Fatalf("bad: %#v", v)
   479  	}
   480  
   481  	expected := 42
   482  	p.SetMeta(42)
   483  	if v := p.Meta(); !reflect.DeepEqual(v, expected) {
   484  		t.Fatalf("bad: %#v", v)
   485  	}
   486  }
   487  
   488  func TestProviderStop(t *testing.T) {
   489  	var p Provider
   490  
   491  	if p.Stopped() {
   492  		t.Fatal("should not be stopped")
   493  	}
   494  
   495  	// Verify stopch blocks
   496  	ch := p.StopContext().Done()
   497  	select {
   498  	case <-ch:
   499  		t.Fatal("should not be stopped")
   500  	case <-time.After(10 * time.Millisecond):
   501  	}
   502  
   503  	// Stop it
   504  	if err := p.Stop(); err != nil {
   505  		t.Fatalf("err: %s", err)
   506  	}
   507  
   508  	// Verify
   509  	if !p.Stopped() {
   510  		t.Fatal("should be stopped")
   511  	}
   512  
   513  	select {
   514  	case <-ch:
   515  	case <-time.After(10 * time.Millisecond):
   516  		t.Fatal("should be stopped")
   517  	}
   518  }
   519  
   520  func TestProviderStop_stopFirst(t *testing.T) {
   521  	var p Provider
   522  
   523  	// Stop it
   524  	if err := p.Stop(); err != nil {
   525  		t.Fatalf("err: %s", err)
   526  	}
   527  
   528  	// Verify
   529  	if !p.Stopped() {
   530  		t.Fatal("should be stopped")
   531  	}
   532  
   533  	select {
   534  	case <-p.StopContext().Done():
   535  	case <-time.After(10 * time.Millisecond):
   536  		t.Fatal("should be stopped")
   537  	}
   538  }
   539  
   540  func TestProviderReset(t *testing.T) {
   541  	var p Provider
   542  	stopCtx := p.StopContext()
   543  	p.MetaReset = func() error {
   544  		stopCtx = p.StopContext()
   545  		return nil
   546  	}
   547  
   548  	// cancel the current context
   549  	p.Stop()
   550  
   551  	if err := p.TestReset(); err != nil {
   552  		t.Fatal(err)
   553  	}
   554  
   555  	// the first context should have been replaced
   556  	if err := stopCtx.Err(); err != nil {
   557  		t.Fatal(err)
   558  	}
   559  
   560  	// we should not get a canceled context here either
   561  	if err := p.StopContext().Err(); err != nil {
   562  		t.Fatal(err)
   563  	}
   564  }
   565  
   566  func TestProvider_InternalValidate(t *testing.T) {
   567  	cases := []struct {
   568  		P           *Provider
   569  		ExpectedErr error
   570  	}{
   571  		{
   572  			P: &Provider{
   573  				Schema: map[string]*Schema{
   574  					"foo": {
   575  						Type:     TypeBool,
   576  						Optional: true,
   577  					},
   578  				},
   579  			},
   580  			ExpectedErr: nil,
   581  		},
   582  		{ // Reserved resource fields should be allowed in provider block
   583  			P: &Provider{
   584  				Schema: map[string]*Schema{
   585  					"provisioner": {
   586  						Type:     TypeString,
   587  						Optional: true,
   588  					},
   589  					"count": {
   590  						Type:     TypeInt,
   591  						Optional: true,
   592  					},
   593  				},
   594  			},
   595  			ExpectedErr: nil,
   596  		},
   597  		{ // Reserved provider fields should not be allowed
   598  			P: &Provider{
   599  				Schema: map[string]*Schema{
   600  					"alias": {
   601  						Type:     TypeString,
   602  						Optional: true,
   603  					},
   604  				},
   605  			},
   606  			ExpectedErr: fmt.Errorf("%s is a reserved field name for a provider", "alias"),
   607  		},
   608  	}
   609  
   610  	for i, tc := range cases {
   611  		err := tc.P.InternalValidate()
   612  		if tc.ExpectedErr == nil {
   613  			if err != nil {
   614  				t.Fatalf("%d: Error returned (expected no error): %s", i, err)
   615  			}
   616  			continue
   617  		}
   618  		if tc.ExpectedErr != nil && err == nil {
   619  			t.Fatalf("%d: Expected error (%s), but no error returned", i, tc.ExpectedErr)
   620  		}
   621  		if err.Error() != tc.ExpectedErr.Error() {
   622  			t.Fatalf("%d: Errors don't match. Expected: %#v Given: %#v", i, tc.ExpectedErr, err)
   623  		}
   624  	}
   625  }