Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Create organisation client to access teams endpoints #135

Merged
merged 13 commits into from
Jul 6, 2023
13 changes: 13 additions & 0 deletions src/main/java/com/spotify/github/v3/clients/GitHubClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.fasterxml.jackson.core.type.TypeReference;
import com.spotify.github.Tracer;
import com.spotify.github.jackson.Json;
import com.spotify.github.v3.Team;
import com.spotify.github.v3.checks.AccessToken;
import com.spotify.github.v3.comment.Comment;
import com.spotify.github.v3.exceptions.ReadOnlyRepositoryException;
Expand Down Expand Up @@ -97,6 +98,9 @@ public class GitHubClient {
new TypeReference<>() {};
static final TypeReference<List<RepositoryInvitation>> LIST_REPOSITORY_INVITATION = new TypeReference<>() {};

static final TypeReference<List<Team>> LIST_TEAMS =
new TypeReference<>() {};

private static final String GET_ACCESS_TOKEN_URL = "app/installations/%s/access_tokens";

private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
Expand Down Expand Up @@ -362,6 +366,15 @@ public ChecksClient createChecksClient(final String owner, final String repo) {
return ChecksClient.create(this, owner, repo);
}

/**
* Create organisation API client
*
* @return organisation API client
*/
public OrganisationClient createOrganisationClient() {
return OrganisationClient.create(this);
}

Json json() {
return json;
}
Expand Down
113 changes: 113 additions & 0 deletions src/main/java/com/spotify/github/v3/clients/OrganisationClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
///*-
// * -\-\-
// * 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.clients;

import static com.spotify.github.v3.clients.GitHubClient.IGNORE_RESPONSE_CONSUMER;
import static com.spotify.github.v3.clients.GitHubClient.LIST_TEAMS;

import com.spotify.github.v3.Team;
import com.spotify.github.v3.orgs.requests.TeamCreate;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OrganisationClient {

private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

private static final String TEAM_TEMPLATE = "/orgs/%s/teams";

private static final String TEAM_SLUG_TEMPLATE = "/orgs/%s/teams/%s";

private final GitHubClient github;

OrganisationClient(final GitHubClient github) {
this.github = github;
}

static OrganisationClient create(final GitHubClient github) {
return new OrganisationClient(github);
}

/**
* Create a team in an organisation.
*
* @param request create team request
* @return team
*/
public CompletableFuture<Team> createTeam(final TeamCreate request, final String org) {
final String path = String.format(TEAM_TEMPLATE, org);
log.debug("Creating team in: " + path);
return github.post(path, github.json().toJsonUnchecked(request), Team.class);
}

/**
* Get a specific team in an organisation.
*
* @param slug slug of the team name
* @param org organisation name
* @return team
*/
public CompletableFuture<Team> getTeam(final String org, final String slug) {
final String path = String.format(TEAM_SLUG_TEMPLATE, org, slug);
log.debug("Fetching team from " + path);
return github.request(path, Team.class);
}

/**
* List teams within an organisation.
*
* @param org organisation name
* @return list of all teams in an organisation
*/
public CompletableFuture<List<Team>> listTeams(final String org) {
final String path = String.format(TEAM_TEMPLATE, org);
log.debug("Fetching teams from " + path);
return github.request(path, LIST_TEAMS);
}

/**
* Update a team in an organisation.
*
* @param request update team request
* @return team
*/
public CompletableFuture<Team> updateTeam(final TeamCreate request, final String org, final String slug) {
final String path = String.format(TEAM_SLUG_TEMPLATE, org, slug);
log.debug("Updating team in: " + path);
return github.patch(path, github.json().toJsonUnchecked(request), Team.class);
}

/**
* Delete a specific team in an organisation.
*
* @param slug slug of the team name
* @param org organisation name
* @return team
*/
public CompletableFuture<Void> deleteTeam(final String org, final String slug) {
final String path = String.format(TEAM_SLUG_TEMPLATE, org, slug);
log.debug("Deleting team from: " + path);
return github.delete(path).thenAccept(IGNORE_RESPONSE_CONSUMER);
}
}
58 changes: 58 additions & 0 deletions src/main/java/com/spotify/github/v3/orgs/requests/TeamCreate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*-
* -\-\-
* 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 java.util.Optional;
import javax.annotation.Nullable;
import org.immutables.value.Value;

/** Request to create a team within a given organisation */
@Value.Immutable
@GithubStyle
@JsonSerialize(as = ImmutableTeamCreate.class)
@JsonDeserialize(as = ImmutableTeamCreate.class)
public interface TeamCreate {

/** The name of the team. */
@Nullable
String name();

/** The description of the team. */
Optional<String> description();

/**
* List GitHub IDs for organization members who will
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IDs is a bit confusing to me. Are these usernames?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GitHub ID is the unique integer associated with a user. Which is unfortunately poorly documented :/
I used the documentation for the GitHub API to describe the params here:
https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#create-a-team

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, then let's leave it as-is 👍

* become team maintainers.
*/
Optional<String> maintainers();

/** The full name (e.g., "organization-name/repository-name")
* of repositories to add the team to.
*/
@SuppressWarnings("checkstyle:methodname")
Optional<String> repo_names();

/** The ID of a team to set as the parent team. */
@SuppressWarnings("checkstyle:methodname")
Optional<String> parent_team_id();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*-
* -\-\-
* 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.clients;

import static com.google.common.io.Resources.getResource;
import static com.spotify.github.v3.clients.GitHubClient.LIST_TEAMS;
import static java.nio.charset.Charset.defaultCharset;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.google.common.io.Resources;
import com.spotify.github.jackson.Json;
import com.spotify.github.v3.Team;
import com.spotify.github.v3.orgs.requests.TeamCreate;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import okhttp3.Headers;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest({ Headers.class, ResponseBody.class, Response.class})
public class OrganisationClientTest {

private GitHubClient github;

private OrganisationClient organisationClient;

private Json json;

private static String getFixture(String resource) throws IOException {
return Resources.toString(getResource(OrganisationClientTest.class, resource), defaultCharset());
}

@Before
public void setUp() {
github = mock(GitHubClient.class);
organisationClient = new OrganisationClient(github);
json = Json.create();
when(github.json()).thenReturn(json);
}

@Test
public void getTeam() throws Exception {
final CompletableFuture<Team> fixture =
completedFuture(json.fromJson(getFixture("team_get.json"), Team.class));
when(github.request("/orgs/github/teams/justice-league", Team.class)).thenReturn(fixture);
final Team team = organisationClient.getTeam("github", "justice-league").get();
assertThat(team.id(), is(1));
assertThat(team.name(), is("Justice League"));
}

@Test
public void listTeams() throws Exception {
final CompletableFuture<List<Team>> fixture =
completedFuture(json.fromJson(getFixture("teams_list.json"), LIST_TEAMS));
when(github.request("/orgs/github/teams", LIST_TEAMS)).thenReturn(fixture);
final List<Team> teams = organisationClient.listTeams("github").get();
assertThat(teams.get(0).slug(), is("justice-league"));
assertThat(teams.get(1).slug(), is("x-men"));
assertThat(teams.size(), is(2));
}

@Test
public void deleteTeam() throws Exception {
final CompletableFuture<Response> response = completedFuture(mock(Response.class));
final ArgumentCaptor<String> capture = ArgumentCaptor.forClass(String.class);
when(github.delete(capture.capture())).thenReturn(response);

CompletableFuture<Void> deleteResponse = organisationClient.deleteTeam("github", "justice-league");
deleteResponse.get();
assertThat(capture.getValue(), is("/orgs/github/teams/justice-league"));
}

@Test
public void createTeam() throws Exception {
final TeamCreate teamCreateRequest =
json.fromJson(
getFixture("teams_request.json"),
TeamCreate.class);

final CompletableFuture<Team> fixtureResponse = completedFuture(json.fromJson(
getFixture("team_get.json"),
Team.class));
when(github.post(any(), any(), eq(Team.class))).thenReturn(fixtureResponse);
final CompletableFuture<Team> actualResponse = organisationClient.createTeam(teamCreateRequest, "github");

assertThat(actualResponse.get().name(), is("Justice League"));
}

@Test
public void updateTeam() throws Exception {
final TeamCreate teamCreateRequest =
json.fromJson(
getFixture("teams_patch.json"),
TeamCreate.class);

final CompletableFuture<Team> fixtureResponse = completedFuture(json.fromJson(
getFixture("teams_patch_response.json"),
Team.class));
when(github.patch(any(), any(), eq(Team.class))).thenReturn(fixtureResponse);
final CompletableFuture<Team> actualResponse = organisationClient.updateTeam(teamCreateRequest, "github", "justice-league");

assertThat(actualResponse.get().name(), is("Justice League2"));
}
}
49 changes: 49 additions & 0 deletions src/test/resources/com/spotify/github/v3/clients/team_get.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"id": 1,
"node_id": "MDQ6VGVhbTE=",
"url": "https://api.github.com/teams/1",
"html_url": "https://github.com/orgs/github/teams/justice-league",
"name": "Justice League",
"slug": "justice-league",
"description": "A great team.",
"privacy": "closed",
"notification_setting": "notifications_enabled",
"permission": "admin",
"members_url": "https://api.github.com/teams/1/members{/member}",
"repositories_url": "https://api.github.com/teams/1/repos",
"parent": null,
"members_count": 3,
"repos_count": 10,
"created_at": "2017-07-14T16:53:42Z",
"updated_at": "2017-08-17T12:37:15Z",
"organization": {
"login": "github",
"id": 1,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjE=",
"url": "https://api.github.com/orgs/github",
"repos_url": "https://api.github.com/orgs/github/repos",
"events_url": "https://api.github.com/orgs/github/events",
"hooks_url": "https://api.github.com/orgs/github/hooks",
"issues_url": "https://api.github.com/orgs/github/issues",
"members_url": "https://api.github.com/orgs/github/members{/member}",
"public_members_url": "https://api.github.com/orgs/github/public_members{/member}",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"description": "A great organization",
"name": "github",
"company": "GitHub",
"blog": "https://github.com/blog",
"location": "San Francisco",
"email": "octocat@github.com",
"is_verified": true,
"has_organization_projects": true,
"has_repository_projects": true,
"public_repos": 2,
"public_gists": 1,
"followers": 20,
"following": 0,
"html_url": "https://github.com/octocat",
"created_at": "2008-01-14T04:33:35Z",
"updated_at": "2017-08-17T12:37:15Z",
"type": "Organization"
}
}
Loading