diff --git a/src/main/java/com/spotify/github/v3/clients/TeamClient.java b/src/main/java/com/spotify/github/v3/clients/TeamClient.java index 630c86de..1dfdff67 100644 --- a/src/main/java/com/spotify/github/v3/clients/TeamClient.java +++ b/src/main/java/com/spotify/github/v3/clients/TeamClient.java @@ -18,7 +18,6 @@ * -/-/- */ - package com.spotify.github.v3.clients; import static com.spotify.github.v3.clients.GitHubClient.*; @@ -28,8 +27,10 @@ import com.spotify.github.v3.User; import com.spotify.github.v3.orgs.Membership; import com.spotify.github.v3.orgs.TeamInvitation; +import com.spotify.github.v3.orgs.requests.ImmutableTeamRepoPermissionUpdate; import com.spotify.github.v3.orgs.requests.MembershipCreate; import com.spotify.github.v3.orgs.requests.TeamCreate; +import com.spotify.github.v3.orgs.requests.TeamRepoPermissionUpdate; import com.spotify.github.v3.orgs.requests.TeamUpdate; import java.lang.invoke.MethodHandles; import java.util.Iterator; @@ -54,6 +55,8 @@ public class TeamClient { private static final String INVITATIONS_TEMPLATE = "/orgs/%s/teams/%s/invitations"; + private static final String REPO_TEMPLATE = "/orgs/%s/teams/%s/repos/%s/%s"; + private final GitHubClient github; private final String org; @@ -75,7 +78,7 @@ static TeamClient create(final GitHubClient github, final String org) { */ public CompletableFuture createTeam(final TeamCreate request) { final String path = String.format(TEAM_TEMPLATE, org); - log.debug("Creating team in: " + path); + log.debug("Creating team in: {}", path); return github.post(path, github.json().toJsonUnchecked(request), Team.class); } @@ -87,7 +90,7 @@ public CompletableFuture createTeam(final TeamCreate request) { */ public CompletableFuture getTeam(final String slug) { final String path = String.format(TEAM_SLUG_TEMPLATE, org, slug); - log.debug("Fetching team from " + path); + log.debug("Fetching team from {}", path); return github.request(path, Team.class); } @@ -98,7 +101,7 @@ public CompletableFuture getTeam(final String slug) { */ public CompletableFuture> listTeams() { final String path = String.format(TEAM_TEMPLATE, org); - log.debug("Fetching teams from " + path); + log.debug("Fetching teams from {}", path); return github.request(path, LIST_TEAMS); } @@ -111,7 +114,7 @@ public CompletableFuture> listTeams() { */ public CompletableFuture updateTeam(final TeamUpdate request, final String slug) { final String path = String.format(TEAM_SLUG_TEMPLATE, org, slug); - log.debug("Updating team in: " + path); + log.debug("Updating team in: {}", path); return github.patch(path, github.json().toJsonUnchecked(request), Team.class); } @@ -123,7 +126,7 @@ public CompletableFuture updateTeam(final TeamUpdate request, final String */ public CompletableFuture deleteTeam(final String slug) { final String path = String.format(TEAM_SLUG_TEMPLATE, org, slug); - log.debug("Deleting team from: " + path); + log.debug("Deleting team from: {}", path); return github.delete(path).thenAccept(IGNORE_RESPONSE_CONSUMER); } @@ -133,9 +136,10 @@ public CompletableFuture deleteTeam(final String slug) { * @param request update membership request * @return membership */ - public CompletableFuture updateMembership(final MembershipCreate request, final String slug, final String username) { + public CompletableFuture updateMembership( + final MembershipCreate request, final String slug, final String username) { final String path = String.format(MEMBERSHIP_TEMPLATE, org, slug, username); - log.debug("Updating membership in: " + path); + log.debug("Updating membership in: {}", path); return github.put(path, github.json().toJsonUnchecked(request), Membership.class); } @@ -148,7 +152,7 @@ public CompletableFuture updateMembership(final MembershipCreate req */ public CompletableFuture getMembership(final String slug, final String username) { final String path = String.format(MEMBERSHIP_TEMPLATE, org, slug, username); - log.debug("Fetching membership for: " + path); + log.debug("Fetching membership for: {}", path); return github.request(path, Membership.class); } @@ -160,7 +164,7 @@ public CompletableFuture getMembership(final String slug, final Stri */ public CompletableFuture> listTeamMembers(final String slug) { final String path = String.format(MEMBERS_TEMPLATE, org, slug); - log.debug("Fetching members for: " + path); + log.debug("Fetching members for: {}", path); return github.request(path, LIST_TEAM_MEMBERS); } @@ -173,7 +177,7 @@ public CompletableFuture> listTeamMembers(final String slug) { */ public Iterator> listTeamMembers(final String slug, final int pageSize) { final String path = String.format(PAGED_MEMBERS_TEMPLATE, org, slug, pageSize); - log.debug("Fetching members for: " + path); + log.debug("Fetching members for: {}", path); return new GithubPageIterator<>(new GithubPage<>(github, path, LIST_TEAM_MEMBERS)); } @@ -185,7 +189,7 @@ public Iterator> listTeamMembers(final String slug, final int pa */ public CompletableFuture deleteMembership(final String slug, final String username) { final String path = String.format(MEMBERSHIP_TEMPLATE, org, slug, username); - log.debug("Deleting membership from: " + path); + log.debug("Deleting membership from: {}", path); return github.delete(path).thenAccept(IGNORE_RESPONSE_CONSUMER); } @@ -197,7 +201,31 @@ public CompletableFuture deleteMembership(final String slug, final String */ public CompletableFuture> listPendingTeamInvitations(final String slug) { final String path = String.format(INVITATIONS_TEMPLATE, org, slug); - log.debug("Fetching pending invitations for: " + path); + log.debug("Fetching pending invitations for: {}", path); return github.request(path, LIST_PENDING_TEAM_INVITATIONS); } + + /** + * Update permissions for a team on a specific repository. + * + * @param slug the team slug + * @param repo the repository name + * @param permission the permission level (pull, push, maintain, triage, admin, or a custom repo defined role name) + * @return void status code 204 if successful + */ + public CompletableFuture updateTeamPermissions( + final String slug, final String repo, final String permission) { + final String path = String.format(REPO_TEMPLATE, org, slug, org, repo); + final TeamRepoPermissionUpdate request = + ImmutableTeamRepoPermissionUpdate.builder() + .org(org) + .repo(repo) + .teamSlug(slug) + .permission(permission) + .build(); + log.debug("Updating team permissions for: {}", path); + return github + .put(path, github.json().toJsonUnchecked(request)) + .thenAccept(IGNORE_RESPONSE_CONSUMER); + } } diff --git a/src/main/java/com/spotify/github/v3/orgs/requests/TeamRepoPermissionUpdate.java b/src/main/java/com/spotify/github/v3/orgs/requests/TeamRepoPermissionUpdate.java new file mode 100644 index 00000000..5263ff5f --- /dev/null +++ b/src/main/java/com/spotify/github/v3/orgs/requests/TeamRepoPermissionUpdate.java @@ -0,0 +1,38 @@ +/*- + * -\-\- + * github-api + * -- + * Copyright (C) 2016 - 2020 Spotify AB + * -- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * -/-/- + */ + +package com.spotify.github.v3.orgs.requests; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.spotify.github.GithubStyle; +import org.immutables.value.Value; + +/** Request to update permissions of a team for a specific repo */ +@Value.Immutable +@GithubStyle +@JsonSerialize(as = ImmutableTeamRepoPermissionUpdate.class) +@JsonDeserialize(as = ImmutableTeamRepoPermissionUpdate.class) +public interface TeamRepoPermissionUpdate { + String org(); + String repo(); + String teamSlug(); + String permission(); +} diff --git a/src/test/java/com/spotify/github/v3/clients/TeamClientTest.java b/src/test/java/com/spotify/github/v3/clients/TeamClientTest.java index a7195fbc..82ba2897 100644 --- a/src/test/java/com/spotify/github/v3/clients/TeamClientTest.java +++ b/src/test/java/com/spotify/github/v3/clients/TeamClientTest.java @@ -21,6 +21,7 @@ package com.spotify.github.v3.clients; import static com.google.common.io.Resources.getResource; +import static com.spotify.github.MockHelper.createMockHttpResponse; import static com.spotify.github.v3.clients.GitHubClient.LIST_PENDING_TEAM_INVITATIONS; import static com.spotify.github.v3.clients.GitHubClient.LIST_TEAMS; import static com.spotify.github.v3.clients.GitHubClient.LIST_TEAM_MEMBERS; @@ -48,6 +49,7 @@ import com.spotify.github.v3.orgs.requests.TeamUpdate; import java.io.IOException; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.BeforeEach; @@ -135,7 +137,10 @@ public void updateTeam() throws Exception { assertThat(actualResponse.get().name(), is("Justice League2")); verify(github, times(1)) - .patch(eq("/orgs/github/teams/justice-league"), eq("{\"name\":\"Justice League2\"}"), eq(Team.class)); + .patch( + eq("/orgs/github/teams/justice-league"), + eq("{\"name\":\"Justice League2\"}"), + eq(Team.class)); } @Test @@ -165,22 +170,24 @@ public void listTeamMembers() throws Exception { @Test public void listTeamMembersPaged() throws Exception { final String firstPageLink = - "; rel=\"next\", ; rel=\"last\""; + "; rel=\"next\", ; rel=\"last\""; final String firstPageBody = - Resources.toString(getResource(this.getClass(), "list_members_page1.json"), defaultCharset()); + Resources.toString( + getResource(this.getClass(), "list_members_page1.json"), defaultCharset()); final HttpResponse firstPageResponse = createMockResponse(firstPageLink, firstPageBody); final String lastPageLink = - "; rel=\"first\", ; rel=\"prev\""; + "; rel=\"first\", ; rel=\"prev\""; final String lastPageBody = - Resources.toString(getResource(this.getClass(), "list_members_page2.json"), defaultCharset()); + Resources.toString( + getResource(this.getClass(), "list_members_page2.json"), defaultCharset()); final HttpResponse lastPageResponse = createMockResponse(lastPageLink, lastPageBody); when(github.request(endsWith("/orgs/github/teams/1/members?per_page=1"))) - .thenReturn(completedFuture(firstPageResponse)); + .thenReturn(completedFuture(firstPageResponse)); when(github.request(endsWith("/orgs/github/teams/1/members?page=2"))) - .thenReturn(completedFuture(lastPageResponse)); + .thenReturn(completedFuture(lastPageResponse)); final Iterable> pageIterator = () -> teamClient.listTeamMembers("1", 1); final List users = Async.streamFromPaginatingIterable(pageIterator).collect(toList()); @@ -229,4 +236,19 @@ public void listPendingTeamInvitations() throws Exception { assertThat(pendingInvitations.get(1).id(), is(2)); assertThat(pendingInvitations.size(), is(2)); } + + @Test + void updateTeamPermissions() { + String apiUrl = "/orgs/github/teams/cat-squad/repos/github/octocat"; + HttpResponse response = createMockHttpResponse(apiUrl, 204, "", Map.of()); + when(github.put(eq(apiUrl), any())).thenReturn(completedFuture(response)); + + teamClient.updateTeamPermissions("cat-squad", "octocat", "pull").join(); + + verify(github, times(1)) + .put( + eq(apiUrl), + eq( + "{\"org\":\"github\",\"repo\":\"octocat\",\"team_slug\":\"cat-squad\",\"permission\":\"pull\"}")); + } }