package schema import ( "fmt" "reflect" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/terraform" ) func TestProvider_impl(t *testing.T) { var _ terraform.ResourceProvider = new(Provider) } func TestProviderGetSchema(t *testing.T) { // This functionality is already broadly tested in core_schema_test.go, // so this is just to ensure that the call passes through correctly. p := &Provider{ Schema: map[string]*Schema{ "bar": { Type: TypeString, Required: true, }, }, ResourcesMap: map[string]*Resource{ "foo": &Resource{ Schema: map[string]*Schema{ "bar": { Type: TypeString, Required: true, }, }, }, }, DataSourcesMap: map[string]*Resource{ "baz": &Resource{ Schema: map[string]*Schema{ "bur": { Type: TypeString, Required: true, }, }, }, }, } want := &terraform.ProviderSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "bar": &configschema.Attribute{ Type: cty.String, Required: true, }, }, BlockTypes: map[string]*configschema.NestedBlock{}, }, ResourceTypes: map[string]*configschema.Block{ "foo": testResource(&configschema.Block{ Attributes: map[string]*configschema.Attribute{ "bar": &configschema.Attribute{ Type: cty.String, Required: true, }, }, BlockTypes: map[string]*configschema.NestedBlock{}, }), }, DataSources: map[string]*configschema.Block{ "baz": testResource(&configschema.Block{ Attributes: map[string]*configschema.Attribute{ "bur": &configschema.Attribute{ Type: cty.String, Required: true, }, }, BlockTypes: map[string]*configschema.NestedBlock{}, }), }, } got, err := p.GetSchema(&terraform.ProviderSchemaRequest{ ResourceTypes: []string{"foo", "bar"}, DataSources: []string{"baz", "bar"}, }) if err != nil { t.Fatalf("unexpected error %s", err) } if !cmp.Equal(got, want, equateEmpty, typeComparer) { t.Error("wrong result:\n", cmp.Diff(got, want, equateEmpty, typeComparer)) } } func TestProviderConfigure(t *testing.T) { cases := []struct { P *Provider Config map[string]interface{} Err bool }{ { P: &Provider{}, Config: nil, Err: false, }, { P: &Provider{ Schema: map[string]*Schema{ "foo": &Schema{ Type: TypeInt, Optional: true, }, }, ConfigureFunc: func(d *ResourceData) (interface{}, error) { if d.Get("foo").(int) == 42 { return nil, nil } return nil, fmt.Errorf("nope") }, }, Config: map[string]interface{}{ "foo": 42, }, Err: false, }, { P: &Provider{ Schema: map[string]*Schema{ "foo": &Schema{ Type: TypeInt, Optional: true, }, }, ConfigureFunc: func(d *ResourceData) (interface{}, error) { if d.Get("foo").(int) == 42 { return nil, nil } return nil, fmt.Errorf("nope") }, }, Config: map[string]interface{}{ "foo": 52, }, Err: true, }, } for i, tc := range cases { c, err := config.NewRawConfig(tc.Config) if err != nil { t.Fatalf("err: %s", err) } err = tc.P.Configure(terraform.NewResourceConfig(c)) if err != nil != tc.Err { t.Fatalf("%d: %s", i, err) } } } func TestProviderResources(t *testing.T) { cases := []struct { P *Provider Result []terraform.ResourceType }{ { P: &Provider{}, Result: []terraform.ResourceType{}, }, { P: &Provider{ ResourcesMap: map[string]*Resource{ "foo": nil, "bar": nil, }, }, Result: []terraform.ResourceType{ terraform.ResourceType{Name: "bar", SchemaAvailable: true}, terraform.ResourceType{Name: "foo", SchemaAvailable: true}, }, }, { P: &Provider{ ResourcesMap: map[string]*Resource{ "foo": nil, "bar": &Resource{Importer: &ResourceImporter{}}, "baz": nil, }, }, Result: []terraform.ResourceType{ terraform.ResourceType{Name: "bar", Importable: true, SchemaAvailable: true}, terraform.ResourceType{Name: "baz", SchemaAvailable: true}, terraform.ResourceType{Name: "foo", SchemaAvailable: true}, }, }, } for i, tc := range cases { actual := tc.P.Resources() if !reflect.DeepEqual(actual, tc.Result) { t.Fatalf("%d: %#v", i, actual) } } } func TestProviderDataSources(t *testing.T) { cases := []struct { P *Provider Result []terraform.DataSource }{ { P: &Provider{}, Result: []terraform.DataSource{}, }, { P: &Provider{ DataSourcesMap: map[string]*Resource{ "foo": nil, "bar": nil, }, }, Result: []terraform.DataSource{ terraform.DataSource{Name: "bar", SchemaAvailable: true}, terraform.DataSource{Name: "foo", SchemaAvailable: true}, }, }, } for i, tc := range cases { actual := tc.P.DataSources() if !reflect.DeepEqual(actual, tc.Result) { t.Fatalf("%d: got %#v; want %#v", i, actual, tc.Result) } } } func TestProviderValidate(t *testing.T) { cases := []struct { P *Provider Config map[string]interface{} Err bool }{ { P: &Provider{ Schema: map[string]*Schema{ "foo": &Schema{}, }, }, Config: nil, Err: true, }, } for i, tc := range cases { c, err := config.NewRawConfig(tc.Config) if err != nil { t.Fatalf("err: %s", err) } _, es := tc.P.Validate(terraform.NewResourceConfig(c)) if len(es) > 0 != tc.Err { t.Fatalf("%d: %#v", i, es) } } } func TestProviderDiff_legacyTimeoutType(t *testing.T) { p := &Provider{ ResourcesMap: map[string]*Resource{ "blah": &Resource{ Schema: map[string]*Schema{ "foo": { Type: TypeInt, Optional: true, }, }, Timeouts: &ResourceTimeout{ Create: DefaultTimeout(10 * time.Minute), }, }, }, } invalidCfg := map[string]interface{}{ "foo": 42, "timeouts": []map[string]interface{}{ map[string]interface{}{ "create": "40m", }, }, } ic, err := config.NewRawConfig(invalidCfg) if err != nil { t.Fatalf("err: %s", err) } _, err = p.Diff( &terraform.InstanceInfo{ Type: "blah", }, nil, terraform.NewResourceConfig(ic), ) if err != nil { t.Fatal(err) } } func TestProviderDiff_invalidTimeoutType(t *testing.T) { p := &Provider{ ResourcesMap: map[string]*Resource{ "blah": &Resource{ Schema: map[string]*Schema{ "foo": { Type: TypeInt, Optional: true, }, }, Timeouts: &ResourceTimeout{ Create: DefaultTimeout(10 * time.Minute), }, }, }, } invalidCfg := map[string]interface{}{ "foo": 42, "timeouts": []interface{}{ map[string]interface{}{ "create": "40m", }, }, } ic, err := config.NewRawConfig(invalidCfg) if err != nil { t.Fatalf("err: %s", err) } _, err = p.Diff( &terraform.InstanceInfo{ Type: "blah", }, nil, terraform.NewResourceConfig(ic), ) if err == nil { t.Fatal("Expected provider.Diff to fail with invalid timeout type") } expectedErrMsg := "Invalid Timeout structure" if !strings.Contains(err.Error(), expectedErrMsg) { t.Fatalf("Unexpected error message: %q\nExpected message to contain %q", err.Error(), expectedErrMsg) } } func TestProviderDiff_timeoutInvalidValue(t *testing.T) { p := &Provider{ ResourcesMap: map[string]*Resource{ "blah": &Resource{ Schema: map[string]*Schema{ "foo": { Type: TypeInt, Optional: true, }, }, Timeouts: &ResourceTimeout{ Create: DefaultTimeout(10 * time.Minute), }, }, }, } invalidCfg := map[string]interface{}{ "foo": 42, "timeouts": map[string]interface{}{ "create": "invalid", }, } ic, err := config.NewRawConfig(invalidCfg) if err != nil { t.Fatalf("err: %s", err) } _, err = p.Diff( &terraform.InstanceInfo{ Type: "blah", }, nil, terraform.NewResourceConfig(ic), ) if err == nil { t.Fatal("Expected provider.Diff to fail with invalid timeout value") } expectedErrMsg := "time: invalid duration invalid" if !strings.Contains(err.Error(), expectedErrMsg) { t.Fatalf("Unexpected error message: %q\nExpected message to contain %q", err.Error(), expectedErrMsg) } } func TestProviderValidateResource(t *testing.T) { cases := []struct { P *Provider Type string Config map[string]interface{} Err bool }{ { P: &Provider{}, Type: "foo", Config: nil, Err: true, }, { P: &Provider{ ResourcesMap: map[string]*Resource{ "foo": &Resource{}, }, }, Type: "foo", Config: nil, Err: false, }, } for i, tc := range cases { c, err := config.NewRawConfig(tc.Config) if err != nil { t.Fatalf("err: %s", err) } _, es := tc.P.ValidateResource(tc.Type, terraform.NewResourceConfig(c)) if len(es) > 0 != tc.Err { t.Fatalf("%d: %#v", i, es) } } } func TestProviderImportState_default(t *testing.T) { p := &Provider{ ResourcesMap: map[string]*Resource{ "foo": &Resource{ Importer: &ResourceImporter{}, }, }, } states, err := p.ImportState(&terraform.InstanceInfo{ Type: "foo", }, "bar") if err != nil { t.Fatalf("err: %s", err) } if len(states) != 1 { t.Fatalf("bad: %#v", states) } if states[0].ID != "bar" { t.Fatalf("bad: %#v", states) } } func TestProviderImportState_setsId(t *testing.T) { var val string stateFunc := func(d *ResourceData, meta interface{}) ([]*ResourceData, error) { val = d.Id() return []*ResourceData{d}, nil } p := &Provider{ ResourcesMap: map[string]*Resource{ "foo": &Resource{ Importer: &ResourceImporter{ State: stateFunc, }, }, }, } _, err := p.ImportState(&terraform.InstanceInfo{ Type: "foo", }, "bar") if err != nil { t.Fatalf("err: %s", err) } if val != "bar" { t.Fatal("should set id") } } func TestProviderImportState_setsType(t *testing.T) { var tVal string stateFunc := func(d *ResourceData, meta interface{}) ([]*ResourceData, error) { d.SetId("foo") tVal = d.State().Ephemeral.Type return []*ResourceData{d}, nil } p := &Provider{ ResourcesMap: map[string]*Resource{ "foo": &Resource{ Importer: &ResourceImporter{ State: stateFunc, }, }, }, } _, err := p.ImportState(&terraform.InstanceInfo{ Type: "foo", }, "bar") if err != nil { t.Fatalf("err: %s", err) } if tVal != "foo" { t.Fatal("should set type") } } func TestProviderMeta(t *testing.T) { p := new(Provider) if v := p.Meta(); v != nil { t.Fatalf("bad: %#v", v) } expected := 42 p.SetMeta(42) if v := p.Meta(); !reflect.DeepEqual(v, expected) { t.Fatalf("bad: %#v", v) } } func TestProviderStop(t *testing.T) { var p Provider if p.Stopped() { t.Fatal("should not be stopped") } // Verify stopch blocks ch := p.StopContext().Done() select { case <-ch: t.Fatal("should not be stopped") case <-time.After(10 * time.Millisecond): } // Stop it if err := p.Stop(); err != nil { t.Fatalf("err: %s", err) } // Verify if !p.Stopped() { t.Fatal("should be stopped") } select { case <-ch: case <-time.After(10 * time.Millisecond): t.Fatal("should be stopped") } } func TestProviderStop_stopFirst(t *testing.T) { var p Provider // Stop it if err := p.Stop(); err != nil { t.Fatalf("err: %s", err) } // Verify if !p.Stopped() { t.Fatal("should be stopped") } select { case <-p.StopContext().Done(): case <-time.After(10 * time.Millisecond): t.Fatal("should be stopped") } } func TestProviderReset(t *testing.T) { var p Provider stopCtx := p.StopContext() p.MetaReset = func() error { stopCtx = p.StopContext() return nil } // cancel the current context p.Stop() if err := p.TestReset(); err != nil { t.Fatal(err) } // the first context should have been replaced if err := stopCtx.Err(); err != nil { t.Fatal(err) } // we should not get a canceled context here either if err := p.StopContext().Err(); err != nil { t.Fatal(err) } } func TestProvider_InternalValidate(t *testing.T) { cases := []struct { P *Provider ExpectedErr error }{ { P: &Provider{ Schema: map[string]*Schema{ "foo": { Type: TypeBool, Optional: true, }, }, }, ExpectedErr: nil, }, { // Reserved resource fields should be allowed in provider block P: &Provider{ Schema: map[string]*Schema{ "provisioner": { Type: TypeString, Optional: true, }, "count": { Type: TypeInt, Optional: true, }, }, }, ExpectedErr: nil, }, { // Reserved provider fields should not be allowed P: &Provider{ Schema: map[string]*Schema{ "alias": { Type: TypeString, Optional: true, }, }, }, ExpectedErr: fmt.Errorf("%s is a reserved field name for a provider", "alias"), }, } for i, tc := range cases { err := tc.P.InternalValidate() if tc.ExpectedErr == nil { if err != nil { t.Fatalf("%d: Error returned (expected no error): %s", i, err) } continue } if tc.ExpectedErr != nil && err == nil { t.Fatalf("%d: Expected error (%s), but no error returned", i, tc.ExpectedErr) } if err.Error() != tc.ExpectedErr.Error() { t.Fatalf("%d: Errors don't match. Expected: %#v Given: %#v", i, tc.ExpectedErr, err) } } }