diff --git a/.gitignore b/.gitignore index 624b0bb..b7ef461 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,8 @@ terraform-provider-launchdarkly output .terraform crash.log +.idea/.gitignore +.idea/misc.xml +.idea/modules.xml +.idea/terraform-provider-launchdarkly.iml +.idea/vcs.xml diff --git a/Makefile b/Makefile index 6a0a086..d92b0f6 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ SOURCES = $(wildcard *.go) +TEST?=./launchdarkly .PHONY: default default: build cross-compile @@ -6,6 +7,7 @@ default: build cross-compile .PHONY: build build: go build + go test $(TEST) -timeout=30s -parallel=4 .PHONY: clean clean: @@ -22,7 +24,5 @@ cross-compile: tar -C output/linux_amd64 -czf output/terraform-provider-launchdarkly_linux_amd64.tar.gz terraform-provider-launchdarkly .PHONY: test -test: build - terraform init - terraform apply - terraform destroy +test: + go test $(TEST) -timeout=30s -parallel=4 diff --git a/README.md b/README.md index 7030ce2..ee7208d 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,7 @@ For the `project` resource you only need the project key. e.g.: `import launchda ## Building the provider Clone the repository, and run `make` at the root of the working copy. + +## Testing the provider +run `make test` at the root of the working copy. + diff --git a/launchdarkly/helper_test.go b/launchdarkly/helper_test.go new file mode 100644 index 0000000..19de0cd --- /dev/null +++ b/launchdarkly/helper_test.go @@ -0,0 +1,136 @@ +package launchdarkly + +import ( + "errors" + "github.com/hashicorp/terraform/helper/schema" + "reflect" + "testing" +) + +func TestParseCompositeID(t *testing.T) { + expectedErr := errors.New("error: Import composite ID requires two parts separated by colon, eg x:y") + + testCases := []struct { + name string + id string + wantedP1 string + wantedP2 string + wantedErr error + }{ + { + name: "expected", + id: "id:test", + wantedP1: "id", + wantedP2: "test", + wantedErr: nil, + }, + { + name: "with more than one separator", + id: "id:test:id", + wantedP1: "id", + wantedP2: "test:id", + wantedErr: nil, + }, + { + name: "without separator", + id: "test", + wantedP1: "", + wantedP2: "", + wantedErr: expectedErr, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + p1, p2, err := parseCompositeID(testCase.id) + testParseCompositeIDVerify(t, p1, p2, err, testCase) + }) + } +} + +func testReadMethod(d *schema.ResourceData, m interface{}) error { return nil } + +func TestResourceImport(t *testing.T) { + resourceKey := "resource" + projectKey := "project" + dTest := new(schema.ResourceData) + dTest.SetId(projectKey + ":" + resourceKey) + + dTestWithError := new(schema.ResourceData) + dTestWithError.SetId(resourceKey) + + wantedResourceData := new(schema.ResourceData) + wantedResourceData.SetId(resourceKey) + wantedResourceData.Set("project_key", projectKey) + wantedResourceData.Set("key", resourceKey) + + expectedErr := errors.New("error: Import composite ID requires two parts separated by colon, eg x:y") + + testCases := []struct { + name string + readMethod importFunc + d *schema.ResourceData + meta interface{} + wantedResourceData []*schema.ResourceData + wantedErr error + }{ + { + name: "expected", + readMethod: testReadMethod, + d: dTest, + meta: nil, + wantedResourceData: []*schema.ResourceData{wantedResourceData}, + wantedErr: nil, + }, + { + name: "with bad resource/project key formatting", + readMethod: testReadMethod, + d: dTestWithError, + meta: nil, + wantedResourceData: nil, + wantedErr: expectedErr, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + resourceData, err := resourceImport(testCase.readMethod, testCase.d, testCase.meta) + testResourceImportVerify(t, resourceData, err, testCase.wantedResourceData, testCase.wantedErr) + }) + } +} + +func testParseCompositeIDVerify(t *testing.T, p1 string, p2 string, err error, testCase struct { + name string + id string + wantedP1 string + wantedP2 string + wantedErr error +}) { + if p1 != testCase.wantedP1 { + t.Errorf("got string (%s) but want (%s)", p1, testCase.wantedP1) + } + + if p2 != testCase.wantedP2 { + t.Errorf("got string (%s) but want (%s)", p2, testCase.wantedP2) + } + + if testCase.wantedErr != nil { + if err.Error() != testCase.wantedErr.Error() { + t.Errorf("got error (%s) but want (%s)", err, testCase.wantedErr) + } + } +} + +func testResourceImportVerify(t *testing.T, resourceData []*schema.ResourceData, err error, wantedResourceData []*schema.ResourceData, wantedErr error) { + + if !reflect.DeepEqual(resourceData, wantedResourceData) { + t.Errorf("resourceData is not equal to wantedResourceData") + } + + if wantedErr != nil { + if err.Error() != wantedErr.Error() { + t.Errorf("got error (%s) but want (%s)", err, wantedErr) + } + } +} diff --git a/launchdarkly/urls_test.go b/launchdarkly/urls_test.go new file mode 100644 index 0000000..0a7c9f0 --- /dev/null +++ b/launchdarkly/urls_test.go @@ -0,0 +1,58 @@ +package launchdarkly + +import ( + "testing" +) + +const launchDarklyApiUrl = "https://app.launchdarkly.com/api/v2/" +const aProjectName = "my-project" + +func TestGetProjectCreateUrl(t *testing.T) { + expectedUrl := launchDarklyApiUrl + "projects" + returnedUrl := getProjectCreateUrl() + if returnedUrl != expectedUrl { + t.Errorf("getProjectCreateUrl expected return value was '%s' but got '%s'", expectedUrl, returnedUrl) + } +} + +func TestGetProjectUrl(t *testing.T) { + expectedUrl := launchDarklyApiUrl + "projects/" + aProjectName + returnedUrl := getProjectUrl(aProjectName) + if returnedUrl != expectedUrl { + t.Errorf("getProjectUrl expected return value was '%s' but got '%s'", expectedUrl, returnedUrl) + } +} + +func TestGetFlagCreateUrl(t *testing.T) { + expectedUrl := launchDarklyApiUrl + "flags/" + aProjectName + returnedUrl := getFlagCreateUrl(aProjectName) + if returnedUrl != expectedUrl { + t.Errorf("getFlagCreateUrl expected return value was '%s' but got '%s'", expectedUrl, returnedUrl) + } +} + +func TestGetFlagUrl(t *testing.T) { + aFlagName := "my-super-flag" + expectedUrl := launchDarklyApiUrl + "flags/" + aProjectName + "/" + aFlagName + returnedUrl := getFlagUrl(aProjectName, aFlagName) + if returnedUrl != expectedUrl { + t.Errorf("getFlagUrl expected return value was '%s' but got '%s'", expectedUrl, returnedUrl) + } +} + +func TestGetEnvironmentCreateUrl(t *testing.T) { + expectedUrl := launchDarklyApiUrl + "projects/" + aProjectName + "/environments" + returnedUrl := getEnvironmentCreateUrl(aProjectName) + if returnedUrl != expectedUrl { + t.Errorf("getEnvironmentCreateUrl expected return value was '%s' but got '%s'", expectedUrl, returnedUrl) + } +} + +func TestGetEnvironmentUrl(t *testing.T) { + anEnvironmentName := "my-marvelous-environment" + expectedUrl := launchDarklyApiUrl + "projects/" + aProjectName + "/environments/" + anEnvironmentName + returnedUrl := getEnvironmentUrl(aProjectName, anEnvironmentName) + if returnedUrl != expectedUrl { + t.Errorf("getEnvironmentUrl expected return value was '%s' but got '%s'", expectedUrl, returnedUrl) + } +} diff --git a/launchdarkly/validations.go b/launchdarkly/validations.go index 61801b9..0b7557f 100644 --- a/launchdarkly/validations.go +++ b/launchdarkly/validations.go @@ -83,7 +83,7 @@ func validateColor(v interface{}, k string) ([]string, []error) { } if !matched { - return nil, []error{errors.New(fmt.Sprintf("%s is not a valid RGB color code: %s", k, value))} + return nil, []error{errors.New(fmt.Sprintf("%s is not a valid HEX color code: %s", k, value))} } return nil, nil diff --git a/launchdarkly/validations_test.go b/launchdarkly/validations_test.go new file mode 100644 index 0000000..f5b2106 --- /dev/null +++ b/launchdarkly/validations_test.go @@ -0,0 +1,184 @@ +package launchdarkly + +import ( + "errors" + "fmt" + "reflect" + "testing" +) + +func TestValidateKey(t *testing.T) { + testCases := []struct { + name string + v interface{} + k string + wantedErr []error + }{ + { + name: "expected", + v: "k", + k: "a-key", + wantedErr: nil, + }, + { + name: "without character", + v: "", + k: "a-key", + wantedErr: []error{errors.New(fmt.Sprintf("%s must be between 1 and 20 characters: %s", "a-key", ""))}, + }, + { + name: "with value more than 20 characters", + v: "a-very-long-value-that-exceeds-20-characters", + k: "a-key", + wantedErr: []error{errors.New(fmt.Sprintf("%s must be between 1 and 20 characters: %s", "a-key", "a-very-long-value-that-exceeds-20-characters"))}, + }, + { + name: "with invalid match", + v: "(#*&$?(*@&$)", + k: "a-key", + wantedErr: []error{errors.New(fmt.Sprintf("%s is not a valid key: %s", "a-key", "(#*&$?(*@&$)"))}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + _, errs := validateKey(testCase.v, testCase.k) + testValidateVerifyGeneric(t, errs, testCase.wantedErr) + }) + } +} + +func TestValidateFeatureFlagKey(t *testing.T) { + testCases := []struct { + name string + v interface{} + k string + wantedErr []error + }{ + { + name: "expected", + v: "k", + k: "a-key", + wantedErr: nil, + }, + { + name: "with invalid match", + v: "(#*&$?(*@&$)", + k: "a-key", + wantedErr: []error{errors.New(fmt.Sprintf("%s is not a valid key: %s", "a-key", "(#*&$?(*@&$)"))}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + _, errs := validateFeatureFlagKey(testCase.v, testCase.k) + testValidateVerifyGeneric(t, errs, testCase.wantedErr) + }) + } +} + +func TestValidateFeatureFlagVariationsType(t *testing.T) { + testCases := []struct { + name string + v interface{} + k string + wantedErr []error + }{ + { + name: "expected", + v: "string", + k: "a-key", + wantedErr: nil, + }, + { + name: "with invalid type as value in string", + v: "long", + k: "a-key", + wantedErr: []error{errors.New(fmt.Sprintf("expected %s to be one of %v, got %s", "a-key", []string{"number", "boolean", "string"}, "long"))}, + }, + { + name: "with invalid type as value", + v: 1, + k: "a-key", + wantedErr: []error{errors.New(fmt.Sprintf("expected %s to be a string", "a-key"))}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + _, errs := validateFeatureFlagVariationsType(testCase.v, testCase.k) + testValidateVerifyGeneric(t, errs, testCase.wantedErr) + }) + } +} + +func TestValidateVariationValue(t *testing.T) { + testCases := []struct { + name string + v interface{} + k string + wantedErr []error + }{ + { + name: "expected", + v: "string", + k: "a-key", + wantedErr: nil, + }, + { + name: "with empty string", + v: "", + k: "a-key", + wantedErr: []error{errors.New(fmt.Sprintf("%s cannot be an empty string", "a-key"))}, + }, + { + name: + "with invalid type as value", + v: 1, + k: "a-key", + wantedErr: []error{errors.New(fmt.Sprintf("expected %s to be a string", "a-key"))}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + _, errs := validateVariationValue(testCase.v, testCase.k) + testValidateVerifyGeneric(t, errs, testCase.wantedErr) + }) + } +} + +func TestValidateColor(t *testing.T) { + testCases := []struct { + name string + v interface{} + k string + wantedErr []error + }{ + { + name: "expected", + v: "FF00FF", + k: "a-key", + wantedErr: nil, + }, + { + name: "with HEX sign before color code", + v: "#FF00FF", + k: "a-key", + wantedErr: []error{errors.New(fmt.Sprintf("%s is not a valid HEX color code: %s", "a-key", "#FF00FF"))}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + _, errs := validateColor(testCase.v, testCase.k) + testValidateVerifyGeneric(t, errs, testCase.wantedErr) + }) + } +} + +func testValidateVerifyGeneric(t *testing.T, errs []error, wantedErr []error) { + if !reflect.DeepEqual(errs, wantedErr) { + t.Errorf("got error (%s) but want (%s)", errs, wantedErr) + } +}