diff --git a/CHANGELOG.md b/CHANGELOG.md index e8b4d3c70..fae653e5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,13 @@ # v1.2.0 (Unreleased) ## Enhancements -* Adds support for reading current state version outputs to StateVersionOutputs, which can be useful for reading outputs when users don't have the necessary permissions to read the entire state. -* Adds Variable Set methods for `ApplyToWorkspaces` and `RemoveFromWorkspaces` [#375](https://github.com/hashicorp/go-tfe/pull/375) +* Adds support for reading current state version outputs to StateVersionOutputs, which can be useful for reading outputs when users don't have the necessary permissions to read the entire state by @brandonc [#370](https://github.com/hashicorp/go-tfe/pull/370) +* Adds Variable Set methods for `ApplyToWorkspaces` and `RemoveFromWorkspaces` by @byronwolfman [#375](https://github.com/hashicorp/go-tfe/pull/375) +* Adds `Names` query param field to `TeamListOptions` by @sebasslash [#393](https://github.com/hashicorp/go-tfe/pull/393) +* Adds `Emails` query param field to `OrganizationMembershipListOptions` by @sebasslash [#393](https://github.com/hashicorp/go-tfe/pull/393) * Adds Run Tasks API support by @glennsarti [#381](https://github.com/hashicorp/go-tfe/pull/381), [#382](https://github.com/hashicorp/go-tfe/pull/382) and [#383](https://github.com/hashicorp/go-tfe/pull/383) + ## Bug fixes * Fixes ignored comment when performing apply, discard, cancel, and force-cancel run actions [#388](https://github.com/hashicorp/go-tfe/pull/388) diff --git a/errors.go b/errors.go index 09593ffbf..f4ed4d766 100644 --- a/errors.go +++ b/errors.go @@ -262,4 +262,8 @@ var ( ErrRequiredWorkspacesList = errors.New("no workspaces list provided") ErrCommentBody = errors.New("comment body is required") + + ErrEmptyTeamName = errors.New("team name can not be empty") + + ErrInvalidEmail = errors.New("email is invalid") ) diff --git a/organization_membership.go b/organization_membership.go index 0c4d3340e..aab19eea5 100644 --- a/organization_membership.go +++ b/organization_membership.go @@ -77,6 +77,9 @@ type OrganizationMembershipListOptions struct { // Optional: A list of relations to include. See available resources // https://www.terraform.io/cloud-docs/api-docs/organization-memberships#available-related-resources Include []OrgMembershipIncludeOpt `url:"include,omitempty"` + + // Optional: A list of organization member emails to filter by. + Emails []string `url:"filter[email],omitempty"` } // OrganizationMembershipCreateOptions represents the options for creating an organization membership. @@ -206,6 +209,10 @@ func (o *OrganizationMembershipListOptions) valid() error { return err } + if err := validateOrgMembershipEmailParams(o.Emails); err != nil { + return err + } + return nil } @@ -229,3 +236,13 @@ func validateOrgMembershipIncludeParams(params []OrgMembershipIncludeOpt) error return nil } + +func validateOrgMembershipEmailParams(emails []string) error { + for _, email := range emails { + if !validEmail(email) { + return ErrInvalidEmail + } + } + + return nil +} diff --git a/organization_membership_integration_test.go b/organization_membership_integration_test.go index 19b799380..fe956f413 100644 --- a/organization_membership_integration_test.go +++ b/organization_membership_integration_test.go @@ -72,6 +72,35 @@ func TestOrganizationMembershipsList(t *testing.T) { assert.Contains(t, ml.Items, memTest2) }) + t.Run("with email filter option", func(t *testing.T) { + _, memTest1Cleanup := createOrganizationMembership(t, client, orgTest) + defer memTest1Cleanup() + memTest2, memTest2Cleanup := createOrganizationMembership(t, client, orgTest) + defer memTest2Cleanup() + + memTest3, memTest3Cleanup := createOrganizationMembership(t, client, orgTest) + defer memTest3Cleanup() + + memTest2.User = &User{ID: memTest2.User.ID} + memTest3.User = &User{ID: memTest3.User.ID} + + ml, err := client.OrganizationMemberships.List(ctx, orgTest.Name, &OrganizationMembershipListOptions{ + Emails: []string{memTest2.Email, memTest3.Email}, + }) + require.NoError(t, err) + + assert.Len(t, ml.Items, 2) + assert.Contains(t, ml.Items, memTest2) + assert.Contains(t, ml.Items, memTest3) + + t.Run("with invalid email", func(t *testing.T) { + ml, err = client.OrganizationMemberships.List(ctx, orgTest.Name, &OrganizationMembershipListOptions{ + Emails: []string{"foobar"}, + }) + assert.Equal(t, err, ErrInvalidEmail) + }) + }) + t.Run("without a valid organization", func(t *testing.T) { ml, err := client.OrganizationMemberships.List(ctx, badIdentifier, nil) assert.Nil(t, ml) diff --git a/team.go b/team.go index aa4a8a6a6..164ef6420 100644 --- a/team.go +++ b/team.go @@ -88,6 +88,9 @@ type TeamListOptions struct { // Optional: A list of relations to include. // https://www.terraform.io/docs/cloud/api/teams.html#available-related-resources Include []TeamIncludeOpt `url:"include,omitempty"` + + // Optional: A list of team names to filter by. + Names []string `url:"filter[names],omitempty"` } // TeamCreateOptions represents the options for creating a team. @@ -263,6 +266,10 @@ func (o *TeamListOptions) valid() error { return err } + if err := validateTeamNames(o.Names); err != nil { + return err + } + return nil } @@ -278,3 +285,13 @@ func validateTeamIncludeParams(params []TeamIncludeOpt) error { return nil } + +func validateTeamNames(names []string) error { + for _, name := range names { + if name == "" { + return ErrEmptyTeamName + } + } + + return nil +} diff --git a/team_integration_test.go b/team_integration_test.go index ae3df9fd6..2aad7518b 100644 --- a/team_integration_test.go +++ b/team_integration_test.go @@ -27,6 +27,8 @@ func TestTeamsList(t *testing.T) { defer tmTest1Cleanup() tmTest2, tmTest2Cleanup := createTeam(t, client, orgTest) defer tmTest2Cleanup() + tmTest3, tmTest3Cleanup := createTeam(t, client, orgTest) + defer tmTest3Cleanup() t.Run("without list options", func(t *testing.T) { tl, err := client.Teams.List(ctx, orgTest.Name, nil) @@ -54,6 +56,22 @@ func TestTeamsList(t *testing.T) { assert.Empty(t, tl.Items) assert.Equal(t, 999, tl.CurrentPage) assert.Equal(t, 2, tl.TotalCount) + + tl, err = client.Teams.List(ctx, orgTest.Name, &TeamListOptions{ + Names: []string{tmTest2.Name, tmTest3.Name}, + }) + + assert.Equal(t, tl.Items, 2) + assert.Contains(t, tl.Items, tmTest2) + assert.Contains(t, tl.Items, tmTest3) + + t.Run("with invalid names query param", func(t *testing.T) { + // should return an error because we've included an empty string + tl, err = client.Teams.List(ctx, orgTest.Name, &TeamListOptions{ + Names: []string{tmTest2.Name, ""}, + }) + assert.Equal(t, err, ErrEmptyTeamName) + }) }) t.Run("without a valid organization", func(t *testing.T) { diff --git a/tfe.go b/tfe.go index 0f7569cb8..22512c0cf 100644 --- a/tfe.go +++ b/tfe.go @@ -551,7 +551,7 @@ func encodeQueryParams(v url.Values) string { sort.Strings(keys) for _, k := range keys { vs := v[k] - if len(vs) > 1 && k == _includeQueryParam { + if len(vs) > 1 && validSliceKey(k) { val := strings.Join(vs, ",") vs = vs[:0] vs = append(vs, val) @@ -909,3 +909,7 @@ func packContents(path string) (*bytes.Buffer, error) { return body, nil } + +func validSliceKey(key string) bool { + return key == _includeQueryParam || strings.Contains(key, "filter[") +} diff --git a/validations.go b/validations.go index 1f2483734..2ca32293a 100644 --- a/validations.go +++ b/validations.go @@ -1,6 +1,7 @@ package tfe import ( + "net/mail" "regexp" ) @@ -20,3 +21,9 @@ func validString(v *string) bool { func validStringID(v *string) bool { return v != nil && reStringID.MatchString(*v) } + +// validEmail checks if the given input is a correct email +func validEmail(v string) bool { + _, err := mail.ParseAddress(v) + return err == nil +}