1 | package edu.ucsb.cs156.frontiers.services; | |
2 | ||
3 | import com.fasterxml.jackson.core.JsonProcessingException; | |
4 | import com.fasterxml.jackson.databind.DeserializationFeature; | |
5 | import com.fasterxml.jackson.databind.JsonNode; | |
6 | import com.fasterxml.jackson.databind.ObjectMapper; | |
7 | import edu.ucsb.cs156.frontiers.entities.Course; | |
8 | import edu.ucsb.cs156.frontiers.entities.Team; | |
9 | import edu.ucsb.cs156.frontiers.enums.TeamStatus; | |
10 | import java.security.NoSuchAlgorithmException; | |
11 | import java.security.spec.InvalidKeySpecException; | |
12 | import java.util.HashMap; | |
13 | import java.util.Map; | |
14 | import lombok.extern.slf4j.Slf4j; | |
15 | import org.springframework.boot.web.client.RestTemplateBuilder; | |
16 | import org.springframework.http.HttpEntity; | |
17 | import org.springframework.http.HttpHeaders; | |
18 | import org.springframework.http.HttpMethod; | |
19 | import org.springframework.http.ResponseEntity; | |
20 | import org.springframework.stereotype.Service; | |
21 | import org.springframework.web.client.HttpClientErrorException; | |
22 | import org.springframework.web.client.RestTemplate; | |
23 | ||
24 | @Slf4j | |
25 | @Service | |
26 | public class GithubTeamService { | |
27 | ||
28 | private final JwtService jwtService; | |
29 | private final ObjectMapper objectMapper; | |
30 | private final RestTemplate restTemplate; | |
31 | ||
32 | public GithubTeamService( | |
33 | JwtService jwtService, ObjectMapper objectMapper, RestTemplateBuilder builder) { | |
34 | this.jwtService = jwtService; | |
35 | this.objectMapper = objectMapper; | |
36 | this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); | |
37 | this.restTemplate = builder.build(); | |
38 | } | |
39 | ||
40 | /** | |
41 | * Creates a team on GitHub if it doesn't exist, or returns the existing team ID. | |
42 | * | |
43 | * @param team The team to create | |
44 | * @param course The course containing the organization | |
45 | * @return The GitHub team ID | |
46 | * @throws JsonProcessingException if there is an error processing JSON | |
47 | * @throws NoSuchAlgorithmException if there is an algorithm error | |
48 | * @throws InvalidKeySpecException if there is a key specification error | |
49 | */ | |
50 | public Integer createOrGetTeamId(Team team, Course course) | |
51 | throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException { | |
52 | // First check if team already exists by getting team info | |
53 | Integer existingTeamId = getTeamId(team.getName(), course); | |
54 |
1
1. createOrGetTeamId : negated conditional → KILLED |
if (existingTeamId != null) { |
55 |
1
1. createOrGetTeamId : replaced Integer return value with 0 for edu/ucsb/cs156/frontiers/services/GithubTeamService::createOrGetTeamId → KILLED |
return existingTeamId; |
56 | } | |
57 | ||
58 | // Create the team if it doesn't exist | |
59 |
1
1. createOrGetTeamId : replaced Integer return value with 0 for edu/ucsb/cs156/frontiers/services/GithubTeamService::createOrGetTeamId → KILLED |
return createTeam(team.getName(), course); |
60 | } | |
61 | ||
62 | /** | |
63 | * Get the org id, given the org name. | |
64 | * | |
65 | * <p>Note: in the future, it would be better to cache this value in the Course row in the | |
66 | * database at the time the Github App is linked to the org, since it doesn't change. | |
67 | * | |
68 | * @param orgName | |
69 | * @param course | |
70 | * @return | |
71 | * @throws JsonProcessingException | |
72 | * @throws NoSuchAlgorithmException | |
73 | * @throws InvalidKeySpecException | |
74 | */ | |
75 | public Integer getOrgId(String orgName, Course course) | |
76 | throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException { | |
77 | String endpoint = "https://api.github.com/orgs/" + orgName; | |
78 | HttpHeaders headers = new HttpHeaders(); | |
79 | String token = jwtService.getInstallationToken(course); | |
80 |
1
1. getOrgId : removed call to org/springframework/http/HttpHeaders::add → KILLED |
headers.add("Authorization", "Bearer " + token); |
81 |
1
1. getOrgId : removed call to org/springframework/http/HttpHeaders::add → KILLED |
headers.add("Accept", "application/vnd.github+json"); |
82 |
1
1. getOrgId : removed call to org/springframework/http/HttpHeaders::add → KILLED |
headers.add("X-GitHub-Api-Version", "2022-11-28"); |
83 | HttpEntity<String> entity = new HttpEntity<>(headers); | |
84 | ||
85 | ResponseEntity<String> response = | |
86 | restTemplate.exchange(endpoint, HttpMethod.GET, entity, String.class); | |
87 | JsonNode responseJson = objectMapper.readTree(response.getBody()); | |
88 |
1
1. getOrgId : replaced Integer return value with 0 for edu/ucsb/cs156/frontiers/services/GithubTeamService::getOrgId → KILLED |
return responseJson.get("id").asInt(); |
89 | } | |
90 | ||
91 | /** | |
92 | * Gets the team ID for a team name, returns null if team doesn't exist. | |
93 | * | |
94 | * @param teamName The name of the team | |
95 | * @param course The course containing the organization | |
96 | * @return The GitHub team ID or null if not found | |
97 | * @throws JsonProcessingException if there is an error processing JSON | |
98 | * @throws NoSuchAlgorithmException if there is an algorithm error | |
99 | * @throws InvalidKeySpecException if there is a key specification error | |
100 | */ | |
101 | public Integer getTeamId(String teamName, Course course) | |
102 | throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException { | |
103 | String endpoint = "https://api.github.com/orgs/" + course.getOrgName() + "/teams/" + teamName; | |
104 | HttpHeaders headers = new HttpHeaders(); | |
105 | String token = jwtService.getInstallationToken(course); | |
106 |
1
1. getTeamId : removed call to org/springframework/http/HttpHeaders::add → KILLED |
headers.add("Authorization", "Bearer " + token); |
107 |
1
1. getTeamId : removed call to org/springframework/http/HttpHeaders::add → KILLED |
headers.add("Accept", "application/vnd.github+json"); |
108 |
1
1. getTeamId : removed call to org/springframework/http/HttpHeaders::add → KILLED |
headers.add("X-GitHub-Api-Version", "2022-11-28"); |
109 | HttpEntity<String> entity = new HttpEntity<>(headers); | |
110 | ||
111 | try { | |
112 | ResponseEntity<String> response = | |
113 | restTemplate.exchange(endpoint, HttpMethod.GET, entity, String.class); | |
114 | JsonNode responseJson = objectMapper.readTree(response.getBody()); | |
115 |
1
1. getTeamId : replaced Integer return value with 0 for edu/ucsb/cs156/frontiers/services/GithubTeamService::getTeamId → KILLED |
return responseJson.get("id").asInt(); |
116 | } catch (HttpClientErrorException e) { | |
117 |
1
1. getTeamId : negated conditional → KILLED |
if (e.getStatusCode().value() == 404) { |
118 |
1
1. getTeamId : replaced Integer return value with 0 for edu/ucsb/cs156/frontiers/services/GithubTeamService::getTeamId → KILLED |
return null; // Team doesn't exist |
119 | } | |
120 | throw e; | |
121 | } | |
122 | } | |
123 | ||
124 | /** | |
125 | * Creates a new team on GitHub. | |
126 | * | |
127 | * @param teamName The name of the team to create | |
128 | * @param course The course containing the organization | |
129 | * @return The GitHub team ID | |
130 | * @throws JsonProcessingException if there is an error processing JSON | |
131 | * @throws NoSuchAlgorithmException if there is an algorithm error | |
132 | * @throws InvalidKeySpecException if there is a key specification error | |
133 | */ | |
134 | private Integer createTeam(String teamName, Course course) | |
135 | throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException { | |
136 | String endpoint = "https://api.github.com/orgs/" + course.getOrgName() + "/teams"; | |
137 | HttpHeaders headers = new HttpHeaders(); | |
138 | String token = jwtService.getInstallationToken(course); | |
139 |
1
1. createTeam : removed call to org/springframework/http/HttpHeaders::add → KILLED |
headers.add("Authorization", "Bearer " + token); |
140 |
1
1. createTeam : removed call to org/springframework/http/HttpHeaders::add → KILLED |
headers.add("Accept", "application/vnd.github+json"); |
141 |
1
1. createTeam : removed call to org/springframework/http/HttpHeaders::add → KILLED |
headers.add("X-GitHub-Api-Version", "2022-11-28"); |
142 | ||
143 | Map<String, Object> body = new HashMap<>(); | |
144 | body.put("name", teamName); | |
145 | body.put("privacy", "closed"); // Teams are private by default | |
146 | String bodyAsJson = objectMapper.writeValueAsString(body); | |
147 | HttpEntity<String> entity = new HttpEntity<>(bodyAsJson, headers); | |
148 | ||
149 | ResponseEntity<String> response = | |
150 | restTemplate.exchange(endpoint, HttpMethod.POST, entity, String.class); | |
151 | JsonNode responseJson = objectMapper.readTree(response.getBody()); | |
152 | Integer teamId = responseJson.get("id").asInt(); | |
153 | log.info( | |
154 | "Created team '{}' with ID {} in organization {}", teamName, teamId, course.getOrgName()); | |
155 |
1
1. createTeam : replaced Integer return value with 0 for edu/ucsb/cs156/frontiers/services/GithubTeamService::createTeam → KILLED |
return teamId; |
156 | } | |
157 | ||
158 | /** | |
159 | * Gets the current team membership status for a user. | |
160 | * | |
161 | * @param githubLogin The GitHub login of the user | |
162 | * @param teamId The GitHub team ID | |
163 | * @param course The course containing the organization | |
164 | * @return The team status of the user | |
165 | * @throws JsonProcessingException if there is an error processing JSON | |
166 | * @throws NoSuchAlgorithmException if there is an algorithm error | |
167 | * @throws InvalidKeySpecException if there is a key specification error | |
168 | */ | |
169 | public TeamStatus getTeamMembershipStatus(String githubLogin, Integer teamId, Course course) | |
170 | throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException { | |
171 |
1
1. getTeamMembershipStatus : negated conditional → KILLED |
if (githubLogin == null) { |
172 |
1
1. getTeamMembershipStatus : replaced return value with null for edu/ucsb/cs156/frontiers/services/GithubTeamService::getTeamMembershipStatus → KILLED |
return TeamStatus.NO_GITHUB_ID; |
173 | } | |
174 | ||
175 | String endpoint = | |
176 | "https://api.github.com/orgs/" | |
177 | + course.getOrgName() | |
178 | + "/teams/" | |
179 | + teamId | |
180 | + "/memberships/" | |
181 | + githubLogin; | |
182 | HttpHeaders headers = new HttpHeaders(); | |
183 | String token = jwtService.getInstallationToken(course); | |
184 |
1
1. getTeamMembershipStatus : removed call to org/springframework/http/HttpHeaders::add → KILLED |
headers.add("Authorization", "Bearer " + token); |
185 |
1
1. getTeamMembershipStatus : removed call to org/springframework/http/HttpHeaders::add → KILLED |
headers.add("Accept", "application/vnd.github+json"); |
186 |
1
1. getTeamMembershipStatus : removed call to org/springframework/http/HttpHeaders::add → KILLED |
headers.add("X-GitHub-Api-Version", "2022-11-28"); |
187 | HttpEntity<String> entity = new HttpEntity<>(headers); | |
188 | ||
189 | try { | |
190 | ResponseEntity<String> response = | |
191 | restTemplate.exchange(endpoint, HttpMethod.GET, entity, String.class); | |
192 | JsonNode responseJson = objectMapper.readTree(response.getBody()); | |
193 | String role = responseJson.get("role").asText(); | |
194 |
2
1. getTeamMembershipStatus : negated conditional → KILLED 2. getTeamMembershipStatus : replaced return value with null for edu/ucsb/cs156/frontiers/services/GithubTeamService::getTeamMembershipStatus → KILLED |
return "maintainer".equalsIgnoreCase(role) |
195 | ? TeamStatus.TEAM_MAINTAINER | |
196 | : TeamStatus.TEAM_MEMBER; | |
197 | } catch (HttpClientErrorException e) { | |
198 |
1
1. getTeamMembershipStatus : negated conditional → KILLED |
if (e.getStatusCode().value() == 404) { |
199 |
1
1. getTeamMembershipStatus : replaced return value with null for edu/ucsb/cs156/frontiers/services/GithubTeamService::getTeamMembershipStatus → KILLED |
return TeamStatus.NOT_ORG_MEMBER; // User is not a member of the team |
200 | } | |
201 | throw e; | |
202 | } | |
203 | } | |
204 | ||
205 | /** | |
206 | * Adds a member to a GitHub team. | |
207 | * | |
208 | * @param githubLogin The GitHub login of the user to add | |
209 | * @param teamSlug The GitHub team slug (name) | |
210 | * @param role The role to assign ("member" or "maintainer") | |
211 | * @param course The course containing the organization | |
212 | * @return The resulting team status | |
213 | * @throws JsonProcessingException if there is an error processing JSON | |
214 | * @throws NoSuchAlgorithmException if there is an algorithm error | |
215 | * @throws InvalidKeySpecException if there is a key specification error | |
216 | */ | |
217 | public TeamStatus addMemberToGithubTeam( | |
218 | String githubLogin, Integer teamId, String role, Course course, Integer orgId) | |
219 | throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException { | |
220 | String endpoint = | |
221 | "https://api.github.com/organizations/" | |
222 | + orgId | |
223 | + "/team/" | |
224 | + teamId | |
225 | + "/memberships/" | |
226 | + githubLogin; | |
227 | HttpHeaders headers = new HttpHeaders(); | |
228 | String token = jwtService.getInstallationToken(course); | |
229 |
1
1. addMemberToGithubTeam : removed call to org/springframework/http/HttpHeaders::add → KILLED |
headers.add("Authorization", "Bearer " + token); |
230 |
1
1. addMemberToGithubTeam : removed call to org/springframework/http/HttpHeaders::add → KILLED |
headers.add("Accept", "application/vnd.github+json"); |
231 |
1
1. addMemberToGithubTeam : removed call to org/springframework/http/HttpHeaders::add → KILLED |
headers.add("X-GitHub-Api-Version", "2022-11-28"); |
232 | ||
233 | Map<String, Object> body = new HashMap<>(); | |
234 | body.put("role", role); | |
235 | String bodyAsJson = objectMapper.writeValueAsString(body); | |
236 | HttpEntity<String> entity = new HttpEntity<>(bodyAsJson, headers); | |
237 | ||
238 | ResponseEntity<String> response = | |
239 | restTemplate.exchange(endpoint, HttpMethod.PUT, entity, String.class); | |
240 | JsonNode responseJson = objectMapper.readTree(response.getBody()); | |
241 | String resultRole = responseJson.get("role").asText(); | |
242 | log.info("Added user '{}' to team ID {} with role '{}'", githubLogin, teamId, resultRole); | |
243 |
2
1. addMemberToGithubTeam : negated conditional → KILLED 2. addMemberToGithubTeam : replaced return value with null for edu/ucsb/cs156/frontiers/services/GithubTeamService::addMemberToGithubTeam → KILLED |
return "maintainer".equalsIgnoreCase(resultRole) |
244 | ? TeamStatus.TEAM_MAINTAINER | |
245 | : TeamStatus.TEAM_MEMBER; | |
246 | } | |
247 | } | |
Mutations | ||
54 |
1.1 |
|
55 |
1.1 |
|
59 |
1.1 |
|
80 |
1.1 |
|
81 |
1.1 |
|
82 |
1.1 |
|
88 |
1.1 |
|
106 |
1.1 |
|
107 |
1.1 |
|
108 |
1.1 |
|
115 |
1.1 |
|
117 |
1.1 |
|
118 |
1.1 |
|
139 |
1.1 |
|
140 |
1.1 |
|
141 |
1.1 |
|
155 |
1.1 |
|
171 |
1.1 |
|
172 |
1.1 |
|
184 |
1.1 |
|
185 |
1.1 |
|
186 |
1.1 |
|
194 |
1.1 2.2 |
|
198 |
1.1 |
|
199 |
1.1 |
|
229 |
1.1 |
|
230 |
1.1 |
|
231 |
1.1 |
|
243 |
1.1 2.2 |