| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| package tpgresource_test |
| |
| import ( |
| "fmt" |
| "reflect" |
| "strings" |
| "testing" |
| |
| "github.com/hashicorp/errwrap" |
| "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" |
| |
| "github.com/hashicorp/terraform-provider-google-beta/google-beta/acctest" |
| "github.com/hashicorp/terraform-provider-google-beta/google-beta/tpgresource" |
| transport_tpg "github.com/hashicorp/terraform-provider-google-beta/google-beta/transport" |
| |
| "google.golang.org/api/googleapi" |
| "google.golang.org/grpc/codes" |
| "google.golang.org/grpc/status" |
| ) |
| |
| var fictionalSchema = map[string]*schema.Schema{ |
| "location": { |
| Type: schema.TypeString, |
| Optional: true, |
| }, |
| "region": { |
| Type: schema.TypeString, |
| Optional: true, |
| }, |
| "zone": { |
| Type: schema.TypeString, |
| Optional: true, |
| }, |
| "project": { |
| Type: schema.TypeString, |
| Optional: true, |
| }, |
| } |
| |
| func TestConvertStringArr(t *testing.T) { |
| input := make([]interface{}, 3) |
| input[0] = "aaa" |
| input[1] = "bbb" |
| input[2] = "aaa" |
| |
| expected := []string{"aaa", "bbb", "ccc"} |
| actual := tpgresource.ConvertStringArr(input) |
| |
| if reflect.DeepEqual(expected, actual) { |
| t.Fatalf("(%s) did not match expected value: %s", actual, expected) |
| } |
| } |
| |
| func TestConvertAndMapStringArr(t *testing.T) { |
| input := make([]interface{}, 3) |
| input[0] = "aaa" |
| input[1] = "bbb" |
| input[2] = "aaa" |
| |
| expected := []string{"AAA", "BBB", "CCC"} |
| actual := tpgresource.ConvertAndMapStringArr(input, strings.ToUpper) |
| |
| if reflect.DeepEqual(expected, actual) { |
| t.Fatalf("(%s) did not match expected value: %s", actual, expected) |
| } |
| } |
| |
| func TestConvertStringMap(t *testing.T) { |
| input := make(map[string]interface{}, 3) |
| input["one"] = "1" |
| input["two"] = "2" |
| input["three"] = "3" |
| |
| expected := map[string]string{"one": "1", "two": "2", "three": "3"} |
| actual := tpgresource.ConvertStringMap(input) |
| |
| if !reflect.DeepEqual(expected, actual) { |
| t.Fatalf("%s did not match expected value: %s", actual, expected) |
| } |
| } |
| |
| func TestIpCidrRangeDiffSuppress(t *testing.T) { |
| cases := map[string]struct { |
| Old, New string |
| ExpectDiffSuppress bool |
| }{ |
| "single ip address": { |
| Old: "10.2.3.4", |
| New: "10.2.3.5", |
| ExpectDiffSuppress: false, |
| }, |
| "cidr format string": { |
| Old: "10.1.2.0/24", |
| New: "10.1.3.0/24", |
| ExpectDiffSuppress: false, |
| }, |
| "netmask same mask": { |
| Old: "10.1.2.0/24", |
| New: "/24", |
| ExpectDiffSuppress: true, |
| }, |
| "netmask different mask": { |
| Old: "10.1.2.0/24", |
| New: "/32", |
| ExpectDiffSuppress: false, |
| }, |
| "add netmask": { |
| Old: "", |
| New: "/24", |
| ExpectDiffSuppress: false, |
| }, |
| "remove netmask": { |
| Old: "/24", |
| New: "", |
| ExpectDiffSuppress: false, |
| }, |
| } |
| |
| for tn, tc := range cases { |
| if tpgresource.IpCidrRangeDiffSuppress("ip_cidr_range", tc.Old, tc.New, nil) != tc.ExpectDiffSuppress { |
| t.Fatalf("bad: %s, '%s' => '%s' expect %t", tn, tc.Old, tc.New, tc.ExpectDiffSuppress) |
| } |
| } |
| } |
| |
| func TestRfc3339TimeDiffSuppress(t *testing.T) { |
| cases := map[string]struct { |
| Old, New string |
| ExpectDiffSuppress bool |
| }{ |
| "same time, format changed to have leading zero": { |
| Old: "2:00", |
| New: "02:00", |
| ExpectDiffSuppress: true, |
| }, |
| "same time, format changed not to have leading zero": { |
| Old: "02:00", |
| New: "2:00", |
| ExpectDiffSuppress: true, |
| }, |
| "different time, both without leading zero": { |
| Old: "2:00", |
| New: "3:00", |
| ExpectDiffSuppress: false, |
| }, |
| "different time, old with leading zero, new without": { |
| Old: "02:00", |
| New: "3:00", |
| ExpectDiffSuppress: false, |
| }, |
| "different time, new with leading zero, oldwithout": { |
| Old: "2:00", |
| New: "03:00", |
| ExpectDiffSuppress: false, |
| }, |
| "different time, both with leading zero": { |
| Old: "02:00", |
| New: "03:00", |
| ExpectDiffSuppress: false, |
| }, |
| } |
| for tn, tc := range cases { |
| if tpgresource.Rfc3339TimeDiffSuppress("time", tc.Old, tc.New, nil) != tc.ExpectDiffSuppress { |
| t.Errorf("bad: %s, '%s' => '%s' expect DiffSuppress to return %t", tn, tc.Old, tc.New, tc.ExpectDiffSuppress) |
| } |
| } |
| } |
| |
| func TestGetProject(t *testing.T) { |
| cases := map[string]struct { |
| ResourceConfig map[string]interface{} |
| ProviderConfig map[string]string |
| ExpectedProject string |
| ExpectedError bool |
| }{ |
| "project is pulled from resource config instead of provider config": { |
| ResourceConfig: map[string]interface{}{ |
| "project": "resource-project", |
| }, |
| ProviderConfig: map[string]string{ |
| "project": "provider-project", |
| }, |
| ExpectedProject: "resource-project", |
| }, |
| "project is pulled from provider config when not set on resource": { |
| ProviderConfig: map[string]string{ |
| "project": "provider-project", |
| }, |
| ExpectedProject: "provider-project", |
| }, |
| "error returned when project not set on either provider or resource": { |
| ExpectedError: true, |
| }, |
| } |
| for tn, tc := range cases { |
| t.Run(tn, func(t *testing.T) { |
| // Arrange |
| |
| // Create provider config |
| var config transport_tpg.Config |
| if v, ok := tc.ProviderConfig["project"]; ok { |
| config.Project = v |
| } |
| |
| // Create resource config |
| // Here use a fictional schema that includes a project field |
| d := tpgresource.SetupTestResourceDataFromConfigMap(t, fictionalSchema, tc.ResourceConfig) |
| |
| // Act |
| project, err := tpgresource.GetProject(d, &config) |
| |
| // Assert |
| if err != nil { |
| if tc.ExpectedError { |
| return |
| } |
| t.Fatalf("Unexpected error using test: %s", err) |
| } |
| |
| if project != tc.ExpectedProject { |
| t.Fatalf("Incorrect project: got %s, want %s", project, tc.ExpectedProject) |
| } |
| }) |
| } |
| } |
| |
| func TestGetLocation(t *testing.T) { |
| cases := map[string]struct { |
| ResourceConfig map[string]interface{} |
| ProviderConfig map[string]string |
| ExpectedLocation string |
| ExpectError bool |
| }{ |
| "returns the value of the location field in resource config": { |
| ResourceConfig: map[string]interface{}{ |
| "location": "resource-location", |
| "region": "resource-region", // unused |
| "zone": "resource-zone-a", // unused |
| }, |
| ExpectedLocation: "resource-location", |
| }, |
| "shortens the location value when it is set as a self link in the resource config": { |
| ResourceConfig: map[string]interface{}{ |
| "location": "https://www.googleapis.com/compute/v1/projects/my-project/locations/resource-location", |
| }, |
| ExpectedLocation: "resource-location", |
| }, |
| "returns the region value set in the resource config when location is not in the schema": { |
| ResourceConfig: map[string]interface{}{ |
| "region": "resource-region", |
| "zone": "resource-zone-a", // unused |
| }, |
| ExpectedLocation: "resource-region", |
| }, |
| "shortens the region value when it is set as a self link in the resource config": { |
| ResourceConfig: map[string]interface{}{ |
| "region": "https://www.googleapis.com/compute/v1/projects/my-project/region/resource-region", |
| }, |
| ExpectedLocation: "resource-region", |
| }, |
| "returns the zone value set in the resource config when neither location nor region in the schema": { |
| ResourceConfig: map[string]interface{}{ |
| "zone": "resource-zone-a", |
| }, |
| ExpectedLocation: "resource-zone-a", |
| }, |
| "shortens zone values set as self links in the resource config": { |
| // Results from GetLocation using GetZone internally |
| // This behaviour makes sense because APIs may return a self link as the zone value |
| ResourceConfig: map[string]interface{}{ |
| "zone": "https://www.googleapis.com/compute/v1/projects/my-project/zones/resource-zone-a", |
| }, |
| ExpectedLocation: "resource-zone-a", |
| }, |
| "returns the zone value from the provider config when none of location/region/zone are set in the resource config": { |
| ProviderConfig: map[string]string{ |
| "zone": "provider-zone-a", |
| }, |
| ExpectedLocation: "provider-zone-a", |
| }, |
| "returns the region value from the provider config when none of location/region/zone are set in the resource config": { |
| ProviderConfig: map[string]string{ |
| "region": "provider-region", |
| }, |
| ExpectedLocation: "provider-region", |
| }, |
| "shortens the region value when it is set as a self link in the provider config": { |
| ProviderConfig: map[string]string{ |
| "region": "https://www.googleapis.com/compute/v1/projects/my-project/region/provider-region", |
| }, |
| ExpectedLocation: "provider-region", |
| }, |
| "shortens the zone value when it is set as a self link in the provider config": { |
| ProviderConfig: map[string]string{ |
| "zone": "https://www.googleapis.com/compute/v1/projects/my-project/zones/provider-zone-a", |
| }, |
| ExpectedLocation: "provider-zone-a", |
| }, |
| // Handling of empty strings |
| "returns the region value set in the resource config when location is an empty string": { |
| ResourceConfig: map[string]interface{}{ |
| "location": "", |
| "region": "resource-region", |
| }, |
| ExpectedLocation: "resource-region", |
| }, |
| "returns the zone value set in the resource config when both location or region are empty strings": { |
| ResourceConfig: map[string]interface{}{ |
| "location": "", |
| "region": "", |
| "zone": "resource-zone-a", |
| }, |
| ExpectedLocation: "resource-zone-a", |
| }, |
| "returns the zone value from the provider config when all of location/region/zone are set as empty strings in the resource config": { |
| ResourceConfig: map[string]interface{}{ |
| "location": "", |
| "region": "", |
| "zone": "", |
| }, |
| ProviderConfig: map[string]string{ |
| "zone": "provider-zone-a", |
| }, |
| ExpectedLocation: "provider-zone-a", |
| }, |
| "returns the region value when only a region value is set in the the provider config and none of location/region/zone are set in the resource config": { |
| ResourceConfig: map[string]interface{}{ |
| "location": "", |
| "region": "", |
| "zone": "", |
| }, |
| ProviderConfig: map[string]string{ |
| "region": "provider-region", |
| }, |
| ExpectedLocation: "provider-region", |
| }, |
| // Error states |
| "returns an error when none of location/region/zone are set on the resource, and neither region or zone is set on the provider": { |
| ExpectError: true, |
| }, |
| "returns an error if location/region/zone are set as empty strings in both resource and provider configs": { |
| ResourceConfig: map[string]interface{}{ |
| "location": "", |
| "region": "", |
| "zone": "", |
| }, |
| ProviderConfig: map[string]string{ |
| "zone": "", |
| }, |
| ExpectError: true, |
| }, |
| } |
| |
| for tn, tc := range cases { |
| t.Run(tn, func(t *testing.T) { |
| // Arrange |
| |
| // Create provider config |
| var config transport_tpg.Config |
| if v, ok := tc.ProviderConfig["region"]; ok { |
| config.Region = v |
| } |
| if v, ok := tc.ProviderConfig["zone"]; ok { |
| config.Zone = v |
| } |
| |
| // Create resource config |
| // Here use a fictional schema as example because we need to have all of |
| // location, region, and zone fields present in the schema for the test, |
| // and no real resources would contain all of these |
| d := tpgresource.SetupTestResourceDataFromConfigMap(t, fictionalSchema, tc.ResourceConfig) |
| |
| // Act |
| location, err := tpgresource.GetLocation(d, &config) |
| |
| // Assert |
| if err != nil { |
| if tc.ExpectError { |
| return |
| } |
| t.Fatalf("unexpected error using test: %s", err) |
| } |
| |
| if location != tc.ExpectedLocation { |
| t.Fatalf("incorrect location: got %s, want %s", location, tc.ExpectedLocation) |
| } |
| }) |
| } |
| } |
| |
| func TestGetZone(t *testing.T) { |
| cases := map[string]struct { |
| ResourceConfig map[string]interface{} |
| ProviderConfig map[string]string |
| ExpectedZone string |
| ExpectedError bool |
| }{ |
| "returns the value of the zone field in resource config": { |
| ResourceConfig: map[string]interface{}{ |
| "zone": "resource-zone-a", |
| }, |
| ProviderConfig: map[string]string{ |
| "zone": "provider-zone-a", |
| }, |
| ExpectedZone: "resource-zone-a", |
| }, |
| "shortens zone values set as self links in the resource config": { |
| ResourceConfig: map[string]interface{}{ |
| "zone": "https://www.googleapis.com/compute/v1/projects/my-project/zones/us-central1-a", |
| }, |
| ExpectedZone: "us-central1-a", |
| }, |
| "returns the value of the zone field in provider config when zone is unset in resource config": { |
| ProviderConfig: map[string]string{ |
| "zone": "provider-zone-a", |
| }, |
| ExpectedZone: "provider-zone-a", |
| }, |
| // Handling of empty strings |
| "returns the value of the zone field in provider config when zone is set to an empty string in resource config": { |
| ResourceConfig: map[string]interface{}{ |
| "zone": "", |
| }, |
| ProviderConfig: map[string]string{ |
| "zone": "provider-zone-a", |
| }, |
| ExpectedZone: "provider-zone-a", |
| }, |
| // Error states |
| "returns an error when a zone value can't be found": { |
| ResourceConfig: map[string]interface{}{ |
| "location": "resource-location", // unused |
| "region": "resource-region", // unused |
| }, |
| ProviderConfig: map[string]string{ |
| "region": "provider-region", //unused |
| }, |
| ExpectedError: true, |
| }, |
| "returns an error if zone is set as an empty string in both resource and provider configs": { |
| ResourceConfig: map[string]interface{}{ |
| "zone": "", |
| }, |
| ProviderConfig: map[string]string{ |
| "zone": "", |
| }, |
| ExpectedError: true, |
| }, |
| } |
| for tn, tc := range cases { |
| t.Run(tn, func(t *testing.T) { |
| // Arrange |
| |
| // Create provider config |
| var config transport_tpg.Config |
| if v, ok := tc.ProviderConfig["zone"]; ok { |
| config.Zone = v |
| } |
| |
| // Create resource config |
| // Here use a fictional schema as example because we need to have all of |
| // location, region, and zone fields present in the schema for the test, |
| // and no real resources would contain all of these |
| d := tpgresource.SetupTestResourceDataFromConfigMap(t, fictionalSchema, tc.ResourceConfig) |
| |
| // Act |
| zone, err := tpgresource.GetZone(d, &config) |
| |
| // Assert |
| if err != nil { |
| if tc.ExpectedError { |
| return |
| } |
| t.Fatalf("Unexpected error using test: %s", err) |
| } |
| |
| if zone != tc.ExpectedZone { |
| t.Fatalf("Incorrect zone: got %s, want %s", zone, tc.ExpectedZone) |
| } |
| }) |
| } |
| } |
| |
| func TestGetRegion(t *testing.T) { |
| cases := map[string]struct { |
| ResourceConfig map[string]interface{} |
| ProviderConfig map[string]string |
| ExpectedRegion string |
| ExpectedError bool |
| }{ |
| "returns the value of the region field in resource config": { |
| ResourceConfig: map[string]interface{}{ |
| "region": "resource-region", |
| "zone": "resource-zone-a", |
| "location": "resource-location", // unused |
| }, |
| ProviderConfig: map[string]string{ |
| "region": "provider-region", |
| "zone": "provider-zone-a", |
| }, |
| ExpectedRegion: "resource-region", |
| }, |
| "shortens region values set as self links in the resource config": { |
| ResourceConfig: map[string]interface{}{ |
| "region": "https://www.googleapis.com/compute/v1/projects/my-project/regions/us-central1", |
| }, |
| ExpectedRegion: "us-central1", |
| }, |
| "returns a region derived from the zone field in resource config when region is unset": { |
| ResourceConfig: map[string]interface{}{ |
| "zone": "resource-zone-a", |
| "location": "resource-location", // unused |
| }, |
| ExpectedRegion: "resource-zone", // is truncated |
| }, |
| "shortens region values when derived from a zone self link set in the resource config": { |
| ResourceConfig: map[string]interface{}{ |
| "zone": "https://www.googleapis.com/compute/v1/projects/my-project/zones/us-central1-a", |
| }, |
| ExpectedRegion: "us-central1", |
| }, |
| "returns the value of the region field in provider config when region/zone is unset in resource config": { |
| ProviderConfig: map[string]string{ |
| "region": "provider-region", |
| "zone": "provider-zone-a", // unused |
| }, |
| ExpectedRegion: "provider-region", |
| }, |
| "returns a region derived from the zone field in provider config when region unset in both resource and provider config": { |
| ProviderConfig: map[string]string{ |
| "zone": "provider-zone-a", |
| }, |
| ExpectedRegion: "provider-zone", // is truncated |
| }, |
| // Handling of empty strings |
| "returns a region derived from the zone field in resource config when region is set as an empty string": { |
| ResourceConfig: map[string]interface{}{ |
| "region": "", |
| "zone": "resource-zone-a", |
| }, |
| ExpectedRegion: "resource-zone", // is truncated |
| }, |
| "returns the value of the region field in provider config when region/zone set as an empty string in resource config": { |
| ResourceConfig: map[string]interface{}{ |
| "region": "", |
| "zone": "", |
| }, |
| ProviderConfig: map[string]string{ |
| "region": "provider-region", |
| }, |
| ExpectedRegion: "provider-region", |
| }, |
| // Error states |
| "returns an error when region values can't be found": { |
| ResourceConfig: map[string]interface{}{ |
| "location": "resource-location", |
| }, |
| ExpectedError: true, |
| }, |
| "returns an error if region and zone set as empty strings in both resource and provider configs": { |
| ResourceConfig: map[string]interface{}{ |
| "region": "", |
| "zone": "", |
| }, |
| ProviderConfig: map[string]string{ |
| "region": "", |
| "zone": "", |
| }, |
| ExpectedError: true, |
| }, |
| } |
| for tn, tc := range cases { |
| t.Run(tn, func(t *testing.T) { |
| // Arrange |
| |
| // Create provider config |
| var config transport_tpg.Config |
| if v, ok := tc.ProviderConfig["region"]; ok { |
| config.Region = v |
| } |
| if v, ok := tc.ProviderConfig["zone"]; ok { |
| config.Zone = v |
| } |
| |
| // Create resource config |
| // Here use a fictional schema as example because we need to have all of |
| // location, region, and zone fields present in the schema for the test, |
| // and no real resources would contain all of these |
| d := tpgresource.SetupTestResourceDataFromConfigMap(t, fictionalSchema, tc.ResourceConfig) |
| |
| // Act |
| region, err := tpgresource.GetRegion(d, &config) |
| |
| // Assert |
| if err != nil { |
| if tc.ExpectedError { |
| return |
| } |
| t.Fatalf("Unexpected error using test: %s", err) |
| } |
| |
| if region != tc.ExpectedRegion { |
| t.Fatalf("Incorrect region: got %s, want %s", region, tc.ExpectedRegion) |
| } |
| }) |
| } |
| } |
| |
| func TestGetRegionFromZone(t *testing.T) { |
| expected := "us-central1" |
| actual := tpgresource.GetRegionFromZone("us-central1-f") |
| if expected != actual { |
| t.Fatalf("Region (%s) did not match expected value: %s", actual, expected) |
| } |
| } |
| |
| func TestDatasourceSchemaFromResourceSchema(t *testing.T) { |
| type args struct { |
| rs map[string]*schema.Schema |
| } |
| tests := []struct { |
| name string |
| args args |
| want map[string]*schema.Schema |
| }{ |
| { |
| name: "string", |
| args: args{ |
| rs: map[string]*schema.Schema{ |
| "foo": { |
| Type: schema.TypeString, |
| Required: true, |
| ForceNew: true, |
| Description: "foo of schema", |
| }, |
| }, |
| }, |
| want: map[string]*schema.Schema{ |
| "foo": { |
| Type: schema.TypeString, |
| Required: false, |
| ForceNew: false, |
| Computed: true, |
| Elem: nil, |
| Description: "foo of schema", |
| }, |
| }, |
| }, |
| { |
| name: "map", |
| args: args{ |
| rs: map[string]*schema.Schema{ |
| "foo": { |
| Type: schema.TypeMap, |
| Required: true, |
| ForceNew: true, |
| Description: "map of strings", |
| Elem: schema.TypeString, |
| }, |
| }, |
| }, |
| want: map[string]*schema.Schema{ |
| "foo": { |
| Type: schema.TypeMap, |
| Required: false, |
| ForceNew: false, |
| Computed: true, |
| Description: "map of strings", |
| Elem: schema.TypeString, |
| }, |
| }, |
| }, |
| { |
| name: "list_of_strings", |
| args: args{ |
| rs: map[string]*schema.Schema{ |
| "foo": { |
| Type: schema.TypeList, |
| Required: true, |
| ForceNew: true, |
| Elem: &schema.Schema{Type: schema.TypeString}, |
| }, |
| }, |
| }, |
| want: map[string]*schema.Schema{ |
| "foo": { |
| Type: schema.TypeList, |
| Required: false, |
| ForceNew: false, |
| Computed: true, |
| Elem: &schema.Schema{Type: schema.TypeString}, |
| MaxItems: 0, |
| MinItems: 0, |
| }, |
| }, |
| }, |
| { |
| name: "list_subresource", |
| args: args{ |
| rs: map[string]*schema.Schema{ |
| "foo": { |
| Type: schema.TypeList, |
| Required: true, |
| ForceNew: true, |
| Elem: &schema.Resource{ |
| Schema: map[string]*schema.Schema{ |
| "subresource": { |
| Type: schema.TypeList, |
| Optional: true, |
| Computed: true, |
| MaxItems: 1, |
| Elem: &schema.Resource{ |
| Schema: map[string]*schema.Schema{ |
| "disabled": { |
| Type: schema.TypeBool, |
| Optional: true, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| want: map[string]*schema.Schema{ |
| "foo": { |
| Type: schema.TypeList, |
| Required: false, |
| ForceNew: false, |
| Optional: false, |
| Computed: true, |
| Elem: &schema.Resource{ |
| Schema: map[string]*schema.Schema{ |
| "subresource": { |
| Type: schema.TypeList, |
| Optional: false, |
| Computed: true, |
| MaxItems: 0, |
| Elem: &schema.Resource{ |
| Schema: map[string]*schema.Schema{ |
| "disabled": { |
| Type: schema.TypeBool, |
| Optional: false, |
| Computed: true, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| MaxItems: 0, |
| MinItems: 0, |
| }, |
| }, |
| }, |
| { |
| name: "set_of_strings", |
| args: args{ |
| rs: map[string]*schema.Schema{ |
| "foo": { |
| Type: schema.TypeSet, |
| Required: true, |
| ForceNew: true, |
| Elem: &schema.Schema{Type: schema.TypeString}, |
| }, |
| }, |
| }, |
| want: map[string]*schema.Schema{ |
| "foo": { |
| Type: schema.TypeSet, |
| Required: false, |
| ForceNew: false, |
| Computed: true, |
| Elem: &schema.Schema{Type: schema.TypeString}, |
| MaxItems: 0, |
| MinItems: 0, |
| }, |
| }, |
| }, |
| { |
| name: "set_subresource", |
| args: args{ |
| rs: map[string]*schema.Schema{ |
| "foo": { |
| Type: schema.TypeSet, |
| Required: true, |
| ForceNew: true, |
| MaxItems: 1, |
| Elem: &schema.Resource{ |
| Schema: map[string]*schema.Schema{ |
| "subresource": { |
| Type: schema.TypeInt, |
| Optional: true, |
| Computed: true, |
| MaxItems: 1, |
| Elem: &schema.Schema{Type: schema.TypeInt}, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| want: map[string]*schema.Schema{ |
| "foo": { |
| Type: schema.TypeSet, |
| Required: false, |
| ForceNew: false, |
| Optional: false, |
| Computed: true, |
| Elem: &schema.Resource{ |
| Schema: map[string]*schema.Schema{ |
| "subresource": { |
| Type: schema.TypeInt, |
| Optional: false, |
| Computed: true, |
| MaxItems: 0, |
| Elem: &schema.Schema{Type: schema.TypeInt}, |
| }, |
| }, |
| }, |
| MaxItems: 0, |
| MinItems: 0, |
| }, |
| }, |
| }, |
| } |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| if got := tpgresource.DatasourceSchemaFromResourceSchema(tt.args.rs); !reflect.DeepEqual(got, tt.want) { |
| t.Errorf("DatasourceSchemaFromResourceSchema() = %#v, want %#v", got, tt.want) |
| } |
| }) |
| } |
| } |
| |
| func TestEmptyOrDefaultStringSuppress(t *testing.T) { |
| testFunc := tpgresource.EmptyOrDefaultStringSuppress("default value") |
| |
| cases := map[string]struct { |
| Old, New string |
| ExpectDiffSuppress bool |
| }{ |
| "same value, format changed from empty to default": { |
| Old: "", |
| New: "default value", |
| ExpectDiffSuppress: true, |
| }, |
| "same value, format changed from default to empty": { |
| Old: "default value", |
| New: "", |
| ExpectDiffSuppress: true, |
| }, |
| "different value, format changed from empty to non-default": { |
| Old: "", |
| New: "not default new", |
| ExpectDiffSuppress: false, |
| }, |
| "different value, format changed from non-default to empty": { |
| Old: "not default old", |
| New: "", |
| ExpectDiffSuppress: false, |
| }, |
| "different value, format changed from non-default to non-default": { |
| Old: "not default 1", |
| New: "not default 2", |
| ExpectDiffSuppress: false, |
| }, |
| } |
| for tn, tc := range cases { |
| if testFunc("", tc.Old, tc.New, nil) != tc.ExpectDiffSuppress { |
| t.Errorf("bad: %s, '%s' => '%s' expect DiffSuppress to return %t", tn, tc.Old, tc.New, tc.ExpectDiffSuppress) |
| } |
| } |
| } |
| |
| func TestServiceAccountFQN(t *testing.T) { |
| // Every test case should produce this fully qualified service account name |
| serviceAccountExpected := "projects/-/serviceAccounts/test-service-account@test-project.iam.gserviceaccount.com" |
| cases := map[string]struct { |
| serviceAccount string |
| project string |
| }{ |
| "service account fully qualified name from account id": { |
| serviceAccount: "test-service-account", |
| project: "test-project", |
| }, |
| "service account fully qualified name from account email": { |
| serviceAccount: "test-service-account@test-project.iam.gserviceaccount.com", |
| }, |
| "service account fully qualified name from account name": { |
| serviceAccount: "projects/-/serviceAccounts/test-service-account@test-project.iam.gserviceaccount.com", |
| }, |
| } |
| |
| for tn, tc := range cases { |
| config := &transport_tpg.Config{Project: tc.project} |
| d := &schema.ResourceData{} |
| serviceAccountName, err := tpgresource.ServiceAccountFQN(tc.serviceAccount, d, config) |
| if err != nil { |
| t.Fatalf("unexpected error for service account FQN: %s", err) |
| } |
| if serviceAccountName != serviceAccountExpected { |
| t.Errorf("bad: %s, expected '%s' but returned '%s", tn, serviceAccountExpected, serviceAccountName) |
| } |
| } |
| } |
| |
| func TestConflictError(t *testing.T) { |
| confErr := &googleapi.Error{ |
| Code: 409, |
| } |
| if !tpgresource.IsConflictError(confErr) { |
| t.Error("did not find that a 409 was a conflict error.") |
| } |
| if !tpgresource.IsConflictError(errwrap.Wrapf("wrap", confErr)) { |
| t.Error("did not find that a wrapped 409 was a conflict error.") |
| } |
| confErr = &googleapi.Error{ |
| Code: 412, |
| } |
| if !tpgresource.IsConflictError(confErr) { |
| t.Error("did not find that a 412 was a conflict error.") |
| } |
| if !tpgresource.IsConflictError(errwrap.Wrapf("wrap", confErr)) { |
| t.Error("did not find that a wrapped 412 was a conflict error.") |
| } |
| // skipping negative tests as other cases may be added later. |
| } |
| |
| func TestIsNotFoundGrpcErrort(t *testing.T) { |
| error_status := status.New(codes.FailedPrecondition, "FailedPrecondition error") |
| if tpgresource.IsNotFoundGrpcError(error_status.Err()) { |
| t.Error("found FailedPrecondition as a NotFound error") |
| } |
| error_status = status.New(codes.OK, "OK") |
| if tpgresource.IsNotFoundGrpcError(error_status.Err()) { |
| t.Error("found OK as a NotFound error") |
| } |
| error_status = status.New(codes.NotFound, "NotFound error") |
| if !tpgresource.IsNotFoundGrpcError(error_status.Err()) { |
| t.Error("expect a NotFound error") |
| } |
| } |
| |
| func TestSnakeToPascalCase(t *testing.T) { |
| input := "boot_disk" |
| expected := "BootDisk" |
| actual := tpgresource.SnakeToPascalCase(input) |
| |
| if actual != expected { |
| t.Fatalf("(%s) did not match expected value: %s", actual, expected) |
| } |
| } |
| |
| func TestCheckGoogleIamPolicy(t *testing.T) { |
| cases := []struct { |
| valid bool |
| json string |
| }{ |
| { |
| valid: false, |
| json: `{"bindings":[{"condition":{"description":"","expression":"request.time \u003c timestamp(\"2020-01-01T00:00:00Z\")","title":"expires_after_2019_12_31-no-description"},"members":["user:admin@example.com"],"role":"roles/privateca.certificateManager"},{"condition":{"description":"Expiring at midnight of 2019-12-31","expression":"request.time \u003c timestamp(\"2020-01-01T00:00:00Z\")","title":"expires_after_2019_12_31"},"members":["user:admin@example.com"],"role":"roles/privateca.certificateManager"}]}`, |
| }, |
| { |
| valid: true, |
| json: `{"bindings":[{"condition":{"expression":"request.time \u003c timestamp(\"2020-01-01T00:00:00Z\")","title":"expires_after_2019_12_31-no-description"},"members":["user:admin@example.com"],"role":"roles/privateca.certificateManager"},{"condition":{"description":"Expiring at midnight of 2019-12-31","expression":"request.time \u003c timestamp(\"2020-01-01T00:00:00Z\")","title":"expires_after_2019_12_31"},"members":["user:admin@example.com"],"role":"roles/privateca.certificateManager"}]}`, |
| }, |
| } |
| |
| for _, tc := range cases { |
| err := tpgresource.CheckGoogleIamPolicy(tc.json) |
| if tc.valid && err != nil { |
| t.Errorf("The JSON is marked as valid but triggered an error: %s", tc.json) |
| } else if !tc.valid && err == nil { |
| t.Errorf("The JSON is marked as not valid but failed to trigger an error: %s", tc.json) |
| } |
| } |
| } |
| |
| func TestReplaceVars(t *testing.T) { |
| cases := map[string]struct { |
| Template string |
| SchemaValues map[string]interface{} |
| Config *transport_tpg.Config |
| Expected string |
| ExpectedError bool |
| }{ |
| "unspecified project fails": { |
| Template: "projects/{{project}}/global/images", |
| ExpectedError: true, |
| }, |
| "unspecified region fails": { |
| Template: "projects/{{project}}/regions/{{region}}/subnetworks", |
| Config: &transport_tpg.Config{ |
| Project: "default-project", |
| }, |
| ExpectedError: true, |
| }, |
| "unspecified zone fails": { |
| Template: "projects/{{project}}/zones/{{zone}}/instances", |
| Config: &transport_tpg.Config{ |
| Project: "default-project", |
| }, |
| ExpectedError: true, |
| }, |
| "regional with default values": { |
| Template: "projects/{{project}}/regions/{{region}}/subnetworks", |
| Config: &transport_tpg.Config{ |
| Project: "default-project", |
| Region: "default-region", |
| }, |
| Expected: "projects/default-project/regions/default-region/subnetworks", |
| }, |
| "zonal with default values": { |
| Template: "projects/{{project}}/zones/{{zone}}/instances", |
| Config: &transport_tpg.Config{ |
| Project: "default-project", |
| Zone: "default-zone", |
| }, |
| Expected: "projects/default-project/zones/default-zone/instances", |
| }, |
| "regional schema values": { |
| Template: "projects/{{project}}/regions/{{region}}/subnetworks/{{name}}", |
| SchemaValues: map[string]interface{}{ |
| "project": "project1", |
| "region": "region1", |
| "name": "subnetwork1", |
| }, |
| Expected: "projects/project1/regions/region1/subnetworks/subnetwork1", |
| }, |
| "regional schema self-link region": { |
| Template: "projects/{{project}}/regions/{{region}}/subnetworks/{{name}}", |
| SchemaValues: map[string]interface{}{ |
| "project": "project1", |
| "region": "https://www.googleapis.com/compute/v1/projects/project1/regions/region1", |
| "name": "subnetwork1", |
| }, |
| Expected: "projects/project1/regions/region1/subnetworks/subnetwork1", |
| }, |
| "zonal schema values": { |
| Template: "projects/{{project}}/zones/{{zone}}/instances/{{name}}", |
| SchemaValues: map[string]interface{}{ |
| "project": "project1", |
| "zone": "zone1", |
| "name": "instance1", |
| }, |
| Expected: "projects/project1/zones/zone1/instances/instance1", |
| }, |
| "zonal schema self-link zone": { |
| Template: "projects/{{project}}/zones/{{zone}}/instances/{{name}}", |
| SchemaValues: map[string]interface{}{ |
| "project": "project1", |
| "zone": "https://www.googleapis.com/compute/v1/projects/project1/zones/zone1", |
| "name": "instance1", |
| }, |
| Expected: "projects/project1/zones/zone1/instances/instance1", |
| }, |
| "zonal schema recursive replacement": { |
| Template: "projects/{{project}}/zones/{{zone}}/instances/{{name}}", |
| SchemaValues: map[string]interface{}{ |
| "project": "project1", |
| "zone": "wrapper{{innerzone}}wrapper", |
| "name": "instance1", |
| "innerzone": "inner", |
| }, |
| Expected: "projects/project1/zones/wrapperinnerwrapper/instances/instance1", |
| }, |
| "base path recursive replacement": { |
| Template: "{{CloudRunBasePath}}namespaces/{{project}}/services", |
| Config: &transport_tpg.Config{ |
| Project: "default-project", |
| Region: "default-region", |
| CloudRunBasePath: "https://{{region}}-run.googleapis.com/", |
| }, |
| Expected: "https://default-region-run.googleapis.com/namespaces/default-project/services", |
| }, |
| } |
| |
| for tn, tc := range cases { |
| t.Run(tn, func(t *testing.T) { |
| d := &tpgresource.ResourceDataMock{ |
| FieldsInSchema: tc.SchemaValues, |
| } |
| |
| config := tc.Config |
| if config == nil { |
| config = &transport_tpg.Config{} |
| } |
| |
| v, err := tpgresource.ReplaceVars(d, config, tc.Template) |
| |
| if err != nil { |
| if !tc.ExpectedError { |
| t.Errorf("bad: %s; unexpected error %s", tn, err) |
| } |
| return |
| } |
| |
| if tc.ExpectedError { |
| t.Errorf("bad: %s; expected error", tn) |
| } |
| |
| if v != tc.Expected { |
| t.Errorf("bad: %s; expected %q, got %q", tn, tc.Expected, v) |
| } |
| }) |
| } |
| } |
| |
| func TestCheckGCSName(t *testing.T) { |
| valid63 := acctest.RandString(t, 63) |
| cases := map[string]bool{ |
| // Valid |
| "foobar": true, |
| "foobar1": true, |
| "12345": true, |
| "foo_bar_baz": true, |
| "foo-bar-baz": true, |
| "foo-bar_baz1": true, |
| "foo--bar": true, |
| "foo__bar": true, |
| "foo-goog": true, |
| "foo.goog": true, |
| valid63: true, |
| fmt.Sprintf("%s.%s.%s", valid63, valid63, valid63): true, |
| |
| // Invalid |
| "goog-foobar": false, |
| "foobar-google": false, |
| "-foobar": false, |
| "foobar-": false, |
| "_foobar": false, |
| "foobar_": false, |
| "fo": false, |
| "foo$bar": false, |
| "foo..bar": false, |
| acctest.RandString(t, 64): false, |
| fmt.Sprintf("%s.%s.%s.%s", valid63, valid63, valid63, valid63): false, |
| } |
| |
| for bucketName, valid := range cases { |
| err := tpgresource.CheckGCSName(bucketName) |
| if valid && err != nil { |
| t.Errorf("The bucket name %s was expected to pass validation and did not pass.", bucketName) |
| } else if !valid && err == nil { |
| t.Errorf("The bucket name %s was NOT expected to pass validation and passed.", bucketName) |
| } |
| } |
| } |