diff --git a/github/resource_github_membership.go b/github/resource_github_membership.go index 914620f600..4955387ba3 100644 --- a/github/resource_github_membership.go +++ b/github/resource_github_membership.go @@ -21,9 +21,10 @@ func resourceGithubMembership() *schema.Resource { Schema: map[string]*schema.Schema{ "username": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: caseInsensitive(), }, "role": { Type: schema.TypeString, diff --git a/github/resource_github_membership_test.go b/github/resource_github_membership_test.go index 3b28c76a48..211f1978c6 100644 --- a/github/resource_github_membership_test.go +++ b/github/resource_github_membership_test.go @@ -2,6 +2,7 @@ package github import ( "context" + "errors" "fmt" "testing" @@ -11,6 +12,10 @@ import ( ) func TestAccGithubMembership_basic(t *testing.T) { + if testCollaborator == "" { + t.Skip("Skipping because `GITHUB_TEST_COLLABORATOR` is not set") + } + var membership github.Membership resource.Test(t, resource.TestCase{ @@ -19,7 +24,7 @@ func TestAccGithubMembership_basic(t *testing.T) { CheckDestroy: testAccCheckGithubMembershipDestroy, Steps: []resource.TestStep{ { - Config: testAccGithubMembershipConfig, + Config: testAccGithubMembershipConfig(testCollaborator), Check: resource.ComposeTestCheckFunc( testAccCheckGithubMembershipExists("github_membership.test_org_membership", &membership), testAccCheckGithubMembershipRoleState("github_membership.test_org_membership", &membership), @@ -29,6 +34,42 @@ func TestAccGithubMembership_basic(t *testing.T) { }) } +func TestAccGithubMembership_caseInsensitive(t *testing.T) { + if testCollaborator == "" { + t.Skip("Skipping because `GITHUB_TEST_COLLABORATOR` is not set") + } + + var membership github.Membership + var otherMembership github.Membership + + otherCase := flipUsernameCase(testCollaborator) + + if testCollaborator == otherCase { + t.Skip("Skipping because `GITHUB_TEST_COLLABORATOR` has no letters to flip case") + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGithubMembershipDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGithubMembershipConfig(testCollaborator), + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubMembershipExists("github_membership.test_org_membership", &membership), + ), + }, + { + Config: testAccGithubMembershipConfig(otherCase), + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubMembershipExists("github_membership.test_org_membership", &otherMembership), + testAccGithubMembershipTheSame(&membership, &otherMembership), + ), + }, + }, + }) +} + func TestAccGithubMembership_importBasic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -36,7 +77,7 @@ func TestAccGithubMembership_importBasic(t *testing.T) { CheckDestroy: testAccCheckGithubMembershipDestroy, Steps: []resource.TestStep{ { - Config: testAccGithubMembershipConfig, + Config: testAccGithubMembershipConfig(testCollaborator), }, { ResourceName: "github_membership.test_org_membership", @@ -134,9 +175,21 @@ func testAccCheckGithubMembershipRoleState(n string, membership *github.Membersh } } -var testAccGithubMembershipConfig string = fmt.Sprintf(` +func testAccGithubMembershipConfig(username string) string { + return fmt.Sprintf(` resource "github_membership" "test_org_membership" { username = "%s" role = "member" } -`, testCollaborator) +`, username) +} + +func testAccGithubMembershipTheSame(orig, other *github.Membership) resource.TestCheckFunc { + return func(s *terraform.State) error { + if *orig.URL != *other.URL { + return errors.New("users are different") + } + + return nil + } +} diff --git a/github/resource_github_repository_collaborator.go b/github/resource_github_repository_collaborator.go index 148b526247..1e0c13011d 100644 --- a/github/resource_github_repository_collaborator.go +++ b/github/resource_github_repository_collaborator.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "strings" "github.com/google/go-github/v25/github" "github.com/hashicorp/terraform/helper/schema" @@ -21,9 +22,10 @@ func resourceGithubRepositoryCollaborator() *schema.Resource { // editing repository collaborators are not supported by github api so forcing new on any changes Schema: map[string]*schema.Schema{ "username": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: caseInsensitive(), }, "repository": { Type: schema.TypeString, @@ -87,6 +89,7 @@ func resourceGithubRepositoryCollaboratorRead(d *schema.ResourceData, meta inter if err != nil { return err } else if invitation != nil { + username = *invitation.Invitee.Login permissionName, err := getInvitationPermission(invitation) if err != nil { return err @@ -112,14 +115,14 @@ func resourceGithubRepositoryCollaboratorRead(d *schema.ResourceData, meta inter } for _, c := range collaborators { - if *c.Login == username { + if strings.ToLower(*c.Login) == strings.ToLower(username) { permissionName, err := getRepoPermission(c.Permissions) if err != nil { return err } d.Set("repository", repoName) - d.Set("username", username) + d.Set("username", c.Login) d.Set("permission", permissionName) return nil } @@ -153,6 +156,7 @@ func resourceGithubRepositoryCollaboratorDelete(d *schema.ResourceData, meta int if err != nil { return err } else if invitation != nil { + username = *invitation.Invitee.Login _, err = client.Repositories.DeleteInvitation(ctx, orgName, repoName, *invitation.ID) return err } @@ -172,7 +176,7 @@ func findRepoInvitation(client *github.Client, ctx context.Context, owner, repo, } for _, i := range invitations { - if *i.Invitee.Login == collaborator { + if strings.ToLower(*i.Invitee.Login) == strings.ToLower(collaborator) { return i, nil } } diff --git a/github/resource_github_repository_collaborator_test.go b/github/resource_github_repository_collaborator_test.go index a1336ca931..8245e931ec 100644 --- a/github/resource_github_repository_collaborator_test.go +++ b/github/resource_github_repository_collaborator_test.go @@ -2,10 +2,13 @@ package github import ( "context" + "errors" "fmt" "regexp" + "strings" "testing" + "github.com/google/go-github/v25/github" "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" @@ -14,6 +17,10 @@ import ( const expectedPermission string = "admin" func TestAccGithubRepositoryCollaborator_basic(t *testing.T) { + if testCollaborator == "" { + t.Skip("Skipping because `GITHUB_TEST_COLLABORATOR` is not set") + } + resourceName := "github_repository_collaborator.test_repo_collaborator" repoName := fmt.Sprintf("tf-acc-test-collab-%s", acctest.RandString(5)) @@ -23,7 +30,7 @@ func TestAccGithubRepositoryCollaborator_basic(t *testing.T) { CheckDestroy: testAccCheckGithubRepositoryCollaboratorDestroy, Steps: []resource.TestStep{ { - Config: testAccGithubRepositoryCollaboratorConfig(repoName), + Config: testAccGithubRepositoryCollaboratorConfig(repoName, testCollaborator), Check: resource.ComposeTestCheckFunc( testAccCheckGithubRepositoryCollaboratorExists(resourceName), testAccCheckGithubRepositoryCollaboratorPermission(resourceName), @@ -35,6 +42,46 @@ func TestAccGithubRepositoryCollaborator_basic(t *testing.T) { }) } +func TestAccGithubRepositoryCollaborator_caseInsensitive(t *testing.T) { + if testCollaborator == "" { + t.Skip("Skipping because `GITHUB_TEST_COLLABORATOR` is not set") + } + + resourceName := "github_repository_collaborator.test_repo_collaborator" + repoName := fmt.Sprintf("tf-acc-test-collab-%s", acctest.RandString(5)) + + var origInvitation github.RepositoryInvitation + var otherInvitation github.RepositoryInvitation + + otherCase := flipUsernameCase(testCollaborator) + + if testCollaborator == otherCase { + t.Skip("Skipping because `GITHUB_TEST_COLLABORATOR` has no letters to flip case") + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGithubRepositoryCollaboratorDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGithubRepositoryCollaboratorConfig(repoName, testCollaborator), + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubRepositoryCollaboratorInvited(repoName, testCollaborator, &origInvitation), + ), + }, + { + Config: testAccGithubRepositoryCollaboratorConfig(repoName, otherCase), + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubRepositoryCollaboratorInvited(repoName, otherCase, &otherInvitation), + resource.TestCheckResourceAttr(resourceName, "username", testCollaborator), + testAccGithubRepositoryCollaboratorTheSame(&origInvitation, &otherInvitation), + ), + }, + }, + }) +} + func TestAccGithubRepositoryCollaborator_importBasic(t *testing.T) { repoName := fmt.Sprintf("tf-acc-test-collab-%s", acctest.RandString(5)) @@ -44,7 +91,7 @@ func TestAccGithubRepositoryCollaborator_importBasic(t *testing.T) { CheckDestroy: testAccCheckGithubRepositoryCollaboratorDestroy, Steps: []resource.TestStep{ { - Config: testAccGithubRepositoryCollaboratorConfig(repoName), + Config: testAccGithubRepositoryCollaboratorConfig(repoName, testCollaborator), }, { ResourceName: "github_repository_collaborator.test_repo_collaborator", @@ -169,7 +216,7 @@ func testAccCheckGithubRepositoryCollaboratorPermission(n string) resource.TestC } } -func testAccGithubRepositoryCollaboratorConfig(repoName string) string { +func testAccGithubRepositoryCollaboratorConfig(repoName, username string) string { return fmt.Sprintf(` resource "github_repository" "test" { name = "%s" @@ -180,5 +227,49 @@ resource "github_repository_collaborator" "test_repo_collaborator" { username = "%s" permission = "%s" } -`, repoName, testCollaborator, expectedPermission) +`, repoName, username, expectedPermission) +} + +func testAccCheckGithubRepositoryCollaboratorInvited(repoName, username string, invitation *github.RepositoryInvitation) resource.TestCheckFunc { + return func(s *terraform.State) error { + opt := &github.ListOptions{PerPage: maxPerPage} + + client := testAccProvider.Meta().(*Organization).client + org := testAccProvider.Meta().(*Organization).name + + for { + invitations, resp, err := client.Repositories.ListInvitations(context.TODO(), org, repoName, opt) + if err != nil { + return errors.New(err.Error()) + } + + if len(invitations) > 1 { + return errors.New(fmt.Sprintf("multiple invitations have been sent for repository %s", repoName)) + } + + for _, i := range invitations { + if strings.ToLower(*i.Invitee.Login) == strings.ToLower(username) { + invitation = i + return nil + } + } + + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + return errors.New(fmt.Sprintf("no invitation found for %s", username)) + } +} + +func testAccGithubRepositoryCollaboratorTheSame(orig, other *github.RepositoryInvitation) resource.TestCheckFunc { + return func(s *terraform.State) error { + if orig.ID != other.ID { + return errors.New("collaborators are different") + } + + return nil + } } diff --git a/github/resource_github_team_membership.go b/github/resource_github_team_membership.go index 64a8ec7a28..bfab4eb865 100644 --- a/github/resource_github_team_membership.go +++ b/github/resource_github_team_membership.go @@ -29,9 +29,10 @@ func resourceGithubTeamMembership() *schema.Resource { ForceNew: true, }, "username": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: caseInsensitive(), }, "role": { Type: schema.TypeString, diff --git a/github/resource_github_team_membership_test.go b/github/resource_github_team_membership_test.go index 62844f2597..9c6ba879d0 100644 --- a/github/resource_github_team_membership_test.go +++ b/github/resource_github_team_membership_test.go @@ -2,6 +2,7 @@ package github import ( "context" + "errors" "fmt" "strconv" "testing" @@ -13,7 +14,12 @@ import ( ) func TestAccGithubTeamMembership_basic(t *testing.T) { + if testCollaborator == "" { + t.Skip("Skipping because `GITHUB_TEST_COLLABORATOR` is not set") + } + var membership github.Membership + randString := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) resource.Test(t, resource.TestCase{ @@ -39,6 +45,44 @@ func TestAccGithubTeamMembership_basic(t *testing.T) { }) } +func TestAccGithubTeamMembership_caseInsensitive(t *testing.T) { + if testCollaborator == "" { + t.Skip("Skipping because `GITHUB_TEST_COLLABORATOR` is not set") + } + + var membership github.Membership + var otherMembership github.Membership + + randString := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + otherCase := flipUsernameCase(testCollaborator) + + if testCollaborator == otherCase { + t.Skip("Skipping because `GITHUB_TEST_COLLABORATOR` has no letters to flip case") + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGithubTeamMembershipDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGithubTeamMembershipConfig(randString, testCollaborator, "member"), + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubTeamMembershipExists("github_team_membership.test_team_membership", &membership), + ), + }, + { + Config: testAccGithubTeamMembershipConfig(randString, otherCase, "member"), + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubTeamMembershipExists("github_team_membership.test_team_membership", &otherMembership), + testAccGithubTeamMembershipTheSame(&membership, &otherMembership), + ), + }, + }, + }) +} + func TestAccGithubTeamMembership_importBasic(t *testing.T) { randString := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) @@ -184,3 +228,13 @@ resource "github_team_membership" "test_team_membership" { } `, username, randString, username, role) } + +func testAccGithubTeamMembershipTheSame(orig, other *github.Membership) resource.TestCheckFunc { + return func(s *terraform.State) error { + if *orig.URL != *other.URL { + return errors.New("users are different") + } + + return nil + } +} diff --git a/github/util.go b/github/util.go index b3d232ddd0..d53e7cb1b1 100644 --- a/github/util.go +++ b/github/util.go @@ -12,6 +12,12 @@ const ( maxPerPage = 100 ) +func caseInsensitive() schema.SchemaDiffSuppressFunc { + return func(k, old, new string, d *schema.ResourceData) bool { + return strings.ToLower(old) == strings.ToLower(new) + } +} + func validateValueFunc(values []string) schema.SchemaValidateFunc { return func(v interface{}, k string) (we []string, errors []error) { value := v.(string) diff --git a/github/util_test.go b/github/util_test.go index 00b29900d7..1f0c33fc27 100644 --- a/github/util_test.go +++ b/github/util_test.go @@ -2,6 +2,7 @@ package github import ( "testing" + "unicode" ) func TestAccGithubUtilRole_validation(t *testing.T) { @@ -56,3 +57,21 @@ func TestAccGithubUtilTwoPartID(t *testing.T) { t.Fatalf("Expected parsed part two bar, actual: %s", parsedPartTwo) } } + +func flipUsernameCase(username string) string { + oc := []rune(username) + + for i, ch := range oc { + if unicode.IsLetter(ch) { + + if unicode.IsUpper(ch) { + oc[i] = unicode.ToLower(ch) + } else { + oc[i] = unicode.ToUpper(ch) + } + break + } + + } + return string(oc) +}