Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions github/resource_github_team_members.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func resourceGithubTeamMembers() *schema.Resource {
"members": {
Type: schema.TypeSet,
Required: true,
Set: hashTeamMember,
Description: "List of users that should be members of the team.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
Expand All @@ -85,6 +86,18 @@ func resourceGithubTeamMembers() *schema.Resource {
}
}

// hashTeamMember keys a members block on the lowercased username. GitHub logins
// are case-insensitive and the API returns the canonical casing, so without this
// a configured login that differs only in case lands on a different set key than
// the one read back, and every plan removes and re-adds the member.
func hashTeamMember(v any) int {
m, _ := v.(map[string]any)
username, _ := m["username"].(string)
role, _ := m["role"].(string)

return schema.HashString(strings.ToLower(username) + ":" + role)
}

func resourceGithubTeamMembersCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
meta, _ := m.(*Owner)
client := meta.v3client
Expand Down
55 changes: 55 additions & 0 deletions github/resource_github_team_members_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,41 @@ resource "github_team_members" "test" {
})
})

t.Run("members_are_case_insensitive", func(t *testing.T) {
t.Parallel()

team := mustCreateTestTeam(t)

config := func(username string) string {
return fmt.Sprintf(`
resource "github_team_members" "test" {
team_slug = "%s"

members {
username = "%s"
role = "member"
}
}
`, team.GetSlug(), username)
}

resource.Test(t, resource.TestCase{
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config(testAccConf.testOrgUser1),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue("github_team_members.test", tfjsonpath.New("members"), knownvalue.SetSizeExact(1)),
},
},
{
Config: config(flipUsernameCase(testAccConf.testOrgUser1)),
PlanOnly: true,
},
},
})
})

t.Run("team_by_id_as_slug", func(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -431,3 +466,23 @@ resource "github_team_members" "test" {
})
})
}

func Test_hashTeamMember(t *testing.T) {
t.Parallel()

member := func(username, role string) map[string]any {
return map[string]any{"username": username, "role": role}
}

if hashTeamMember(member("OctoCat", "member")) != hashTeamMember(member("octocat", "member")) {
t.Error("expected the same hash for a username that differs only in case")
}

if hashTeamMember(member("OctoCat", "member")) == hashTeamMember(member("OctoCat", "maintainer")) {
t.Error("expected different hashes for different roles")
}

if hashTeamMember(member("OctoCat", "member")) == hashTeamMember(member("octodog", "member")) {
t.Error("expected different hashes for different usernames")
}
}
Loading