From 065461b69e5bb7d062003750ddee126fdd710f92 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Wed, 4 May 2022 15:42:40 -0400 Subject: [PATCH 1/4] Add filter[names] query param to list opts --- errors.go | 2 ++ team.go | 17 +++++++++++++++++ team_integration_test.go | 15 +++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/errors.go b/errors.go index 09593ffbf..50ce388a6 100644 --- a/errors.go +++ b/errors.go @@ -262,4 +262,6 @@ var ( ErrRequiredWorkspacesList = errors.New("no workspaces list provided") ErrCommentBody = errors.New("comment body is required") + + ErrEmptyTeamName = errors.New("team name can not be empty") ) 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..44b52c4ed 100644 --- a/team_integration_test.go +++ b/team_integration_test.go @@ -54,6 +54,21 @@ 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}, + }) + + assert.Equal(t, tl.Items, 1) + assert.Contains(t, tl.Items, tmTest2) + + 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) { From ea83e1f57e0fba4d175d3457f22694e7a9446821 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Wed, 4 May 2022 17:28:47 -0400 Subject: [PATCH 2/4] Add filter[email] query param to list opts --- errors.go | 2 ++ organization_membership.go | 17 ++++++++++++++++ organization_membership_integration_test.go | 22 +++++++++++++++++++++ validations.go | 7 +++++++ 4 files changed, 48 insertions(+) diff --git a/errors.go b/errors.go index 50ce388a6..f4ed4d766 100644 --- a/errors.go +++ b/errors.go @@ -264,4 +264,6 @@ var ( 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..1e47ab851 100644 --- a/organization_membership_integration_test.go +++ b/organization_membership_integration_test.go @@ -72,6 +72,28 @@ 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() + + ml, err := client.OrganizationMemberships.List(ctx, orgTest.Name, &OrganizationMembershipListOptions{ + Emails: []string{memTest2.Email}, + }) + require.NoError(t, err) + + assert.Len(t, ml.Items, 1) + assert.Equal(t, ml.Items[0].ID, memTest2.ID) + + 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/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 +} From 72b43bc6230e1ee290dedbec488bec9ca8393345 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Wed, 4 May 2022 17:34:41 -0400 Subject: [PATCH 3/4] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1be3675f3..f9262a490 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## 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 `Names` query param field to `TeamListOptions` [#393](https://github.com/hashicorp/go-tfe/pull/393) +* Adds `Emails` query param field to `OrganizationMembershipListOptions` [#393](https://github.com/hashicorp/go-tfe/pull/393) ## Bug fixes * Fixes ignored comment when performing apply, discard, cancel, and force-cancel run actions [#388](https://github.com/hashicorp/go-tfe/pull/388) From 1f33aab5966599f09744554bce13a649ef24a3b6 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Thu, 5 May 2022 12:11:08 -0400 Subject: [PATCH 4/4] Format filter param slice as comma separated string --- organization_membership_integration_test.go | 13 ++++++++++--- team_integration_test.go | 7 +++++-- tfe.go | 6 +++++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/organization_membership_integration_test.go b/organization_membership_integration_test.go index 1e47ab851..fe956f413 100644 --- a/organization_membership_integration_test.go +++ b/organization_membership_integration_test.go @@ -78,13 +78,20 @@ func TestOrganizationMembershipsList(t *testing.T) { 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}, + Emails: []string{memTest2.Email, memTest3.Email}, }) require.NoError(t, err) - assert.Len(t, ml.Items, 1) - assert.Equal(t, ml.Items[0].ID, memTest2.ID) + 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{ diff --git a/team_integration_test.go b/team_integration_test.go index 44b52c4ed..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) @@ -56,11 +58,12 @@ func TestTeamsList(t *testing.T) { assert.Equal(t, 2, tl.TotalCount) tl, err = client.Teams.List(ctx, orgTest.Name, &TeamListOptions{ - Names: []string{tmTest2.Name}, + Names: []string{tmTest2.Name, tmTest3.Name}, }) - assert.Equal(t, tl.Items, 1) + 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 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[") +}