RepositoryService.java

1
package edu.ucsb.cs156.frontiers.services;
2
3
import com.fasterxml.jackson.core.JsonProcessingException;
4
import com.fasterxml.jackson.databind.ObjectMapper;
5
import edu.ucsb.cs156.frontiers.entities.Course;
6
import edu.ucsb.cs156.frontiers.entities.CourseStaff;
7
import edu.ucsb.cs156.frontiers.entities.RosterStudent;
8
import edu.ucsb.cs156.frontiers.entities.Team;
9
import edu.ucsb.cs156.frontiers.enums.RepositoryPermissions;
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.*;
17
import org.springframework.stereotype.Service;
18
import org.springframework.web.client.HttpClientErrorException;
19
import org.springframework.web.client.RestTemplate;
20
21
@Service
22
@Slf4j
23
public class RepositoryService {
24
  private final JwtService jwtService;
25
  private final RestTemplate restTemplate;
26
  private final ObjectMapper mapper;
27
28
  /**
29
   * Creates a GitHub repository for a user (student or staff), given only their GitHub login.
30
   *
31
   * <p>This helper method contains the shared logic used by both {@link
32
   * #createStudentRepository(Course, RosterStudent, String, Boolean, RepositoryPermissions)} and
33
   * {@link #createStaffRepository(Course, CourseStaff, String, Boolean, RepositoryPermissions)}.
34
   *
35
   * <ul>
36
   *   <li>Checks whether the repository already exists.
37
   *   <li>If not, creates a new repository under the course's organization.
38
   *   <li>Adds the user as a collaborator with the given permission level.
39
   * </ul>
40
   *
41
   * @param course the course whose organization the repo belongs to
42
   * @param githubLogin GitHub username of the student or staff member
43
   * @param repoPrefix prefix for the repository name (repoPrefix-githubLogin)
44
   * @param isPrivate whether the created repository should be private
45
   * @param permissions collaborator permissions to grant the user
46
   * @throws NoSuchAlgorithmException if signing fails
47
   * @throws InvalidKeySpecException if signing fails
48
   * @throws JsonProcessingException if JSON serialization fails
49
   */
50
  private void createRepositoryForStudentOrStaff(
51
      Course course,
52
      String githubLogin,
53
      String repoPrefix,
54
      Boolean isPrivate,
55
      RepositoryPermissions permissions)
56
      throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
57
58
    String newRepoName = repoPrefix + "-" + githubLogin;
59
    String token = jwtService.getInstallationToken(course);
60
61
    String existenceEndpoint =
62
        "https://api.github.com/repos/" + course.getOrgName() + "/" + newRepoName;
63
    String createEndpoint = "https://api.github.com/orgs/" + course.getOrgName() + "/repos";
64
    String provisionEndpoint =
65
        "https://api.github.com/repos/"
66
            + course.getOrgName()
67
            + "/"
68
            + newRepoName
69
            + "/collaborators/"
70
            + githubLogin;
71
72
    HttpHeaders existenceHeaders = new HttpHeaders();
73 1 1. createRepositoryForStudentOrStaff : removed call to org/springframework/http/HttpHeaders::add → KILLED
    existenceHeaders.add("Authorization", "Bearer " + token);
74 1 1. createRepositoryForStudentOrStaff : removed call to org/springframework/http/HttpHeaders::add → KILLED
    existenceHeaders.add("Accept", "application/vnd.github+json");
75 1 1. createRepositoryForStudentOrStaff : removed call to org/springframework/http/HttpHeaders::add → KILLED
    existenceHeaders.add("X-GitHub-Api-Version", "2022-11-28");
76
77
    HttpEntity<String> existenceEntity = new HttpEntity<>(existenceHeaders);
78
79
    try {
80
      restTemplate.exchange(existenceEndpoint, HttpMethod.GET, existenceEntity, String.class);
81
    } catch (HttpClientErrorException e) {
82 1 1. createRepositoryForStudentOrStaff : negated conditional → KILLED
      if (e.getStatusCode().equals(HttpStatus.NOT_FOUND)) {
83
        HttpHeaders createHeaders = new HttpHeaders();
84 1 1. createRepositoryForStudentOrStaff : removed call to org/springframework/http/HttpHeaders::add → KILLED
        createHeaders.add("Authorization", "Bearer " + token);
85 1 1. createRepositoryForStudentOrStaff : removed call to org/springframework/http/HttpHeaders::add → KILLED
        createHeaders.add("Accept", "application/vnd.github+json");
86 1 1. createRepositoryForStudentOrStaff : removed call to org/springframework/http/HttpHeaders::add → KILLED
        createHeaders.add("X-GitHub-Api-Version", "2022-11-28");
87
88
        Map<String, Object> body = new HashMap<>();
89
        body.put("name", newRepoName);
90
        body.put("private", isPrivate);
91
        String bodyAsJson = mapper.writeValueAsString(body);
92
93
        HttpEntity<String> createEntity = new HttpEntity<>(bodyAsJson, createHeaders);
94
95
        restTemplate.exchange(createEndpoint, HttpMethod.POST, createEntity, String.class);
96
      } else {
97
        log.warn(
98
            "Unexpected response code {} when checking for existence of repository {}",
99
            e.getStatusCode(),
100
            newRepoName);
101
        return;
102
      }
103
    }
104
105
    try {
106
      Map<String, Object> provisionBody = new HashMap<>();
107
      provisionBody.put("permission", permissions.getApiName());
108
      String provisionAsJson = mapper.writeValueAsString(provisionBody);
109
110
      HttpEntity<String> provisionEntity = new HttpEntity<>(provisionAsJson, existenceHeaders);
111
      restTemplate.exchange(provisionEndpoint, HttpMethod.PUT, provisionEntity, String.class);
112
    } catch (HttpClientErrorException ignored) {
113
      // silently ignore if provisioning fails (same as before)
114
    }
115
  }
116
117
  public RepositoryService(
118
      JwtService jwtService, RestTemplateBuilder restTemplateBuilder, ObjectMapper mapper) {
119
    this.jwtService = jwtService;
120
    this.restTemplate = restTemplateBuilder.build();
121
    this.mapper = mapper;
122
  }
123
124
  /**
125
   * Creates a single student repository if it doesn't already exist, and provisions access to the
126
   * repository by that student
127
   *
128
   * @param course The Course in question
129
   * @param student RosterStudent of the student the repository should be created for
130
   * @param repoPrefix Name of the project or assignment. Used to title the repository, in the
131
   *     format repoPrefix-githubLogin
132
   * @param isPrivate Whether the repository is private or not
133
   */
134
  public void createStudentRepository(
135
      Course course,
136
      RosterStudent student,
137
      String repoPrefix,
138
      Boolean isPrivate,
139
      RepositoryPermissions permissions)
140
      throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
141 1 1. createStudentRepository : removed call to edu/ucsb/cs156/frontiers/services/RepositoryService::createRepositoryForStudentOrStaff → KILLED
    createRepositoryForStudentOrStaff(
142
        course, student.getGithubLogin(), repoPrefix, isPrivate, permissions);
143
  }
144
145
  /**
146
   * Creates a single staff repository if it doesn't already exist, and provisions access to the
147
   * repository by that staff member
148
   *
149
   * @param course The Course in question
150
   * @param staff CourseStaff of the staff the repository should be created for
151
   * @param repoPrefix Name of the project or assignment. Used to title the repository, in the
152
   *     format repoPrefix-githubLogin
153
   * @param isPrivate Whether the repository is private or not
154
   */
155
  public void createStaffRepository(
156
      Course course,
157
      CourseStaff staff,
158
      String repoPrefix,
159
      Boolean isPrivate,
160
      RepositoryPermissions permissions)
161
      throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
162
163 1 1. createStaffRepository : removed call to edu/ucsb/cs156/frontiers/services/RepositoryService::createRepositoryForStudentOrStaff → KILLED
    createRepositoryForStudentOrStaff(
164
        course, staff.getGithubLogin(), repoPrefix, isPrivate, permissions);
165
  }
166
167
  /**
168
   * Creates a GitHub repository for a team (student or staff), given only their team name.
169
   *
170
   * <ul>
171
   *   <li>Checks whether the repository already exists.
172
   *   <li>If not, creates a new repository under the course's organization.
173
   *   <li>Adds all team members as collaborators with the given permission level.
174
   * </ul>
175
   *
176
   * @param course the course whose organization the repo belongs to
177
   * @param team the team for which the repo is being created
178
   * @param repoPrefix prefix for the repository name (repoPrefix-teamName)
179
   * @param isPrivate whether the created repository should be private
180
   * @param permissions collaborator permissions to grant the user
181
   * @throws NoSuchAlgorithmException if signing fails
182
   * @throws InvalidKeySpecException if signing fails
183
   * @throws JsonProcessingException if JSON serialization fails
184
   */
185
  public void createTeamRepository(
186
      Course course,
187
      Team team,
188
      String repoPrefix,
189
      Boolean isPrivate,
190
      RepositoryPermissions permissions)
191
      throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
192
    // Should update Team entity with a team slug field. Would need to update all instances in code
193
    // base where teams are created
194
    String teamSlug = team.getName().toLowerCase().replaceAll("[^a-z0-9-]+", "-");
195
    String newRepoName = repoPrefix + "-" + teamSlug;
196
    String token = jwtService.getInstallationToken(course);
197
198
    String existenceEndpoint =
199
        "https://api.github.com/repos/" + course.getOrgName() + "/" + newRepoName;
200
    String createEndpoint = "https://api.github.com/orgs/" + course.getOrgName() + "/repos";
201
    String provisionEndpoint =
202
        "https://api.github.com/teams/"
203
            + team.getGithubTeamId()
204
            + "/repos/"
205
            + course.getOrgName()
206
            + "/"
207
            + newRepoName;
208
209
    HttpHeaders existenceHeaders = new HttpHeaders();
210 1 1. createTeamRepository : removed call to org/springframework/http/HttpHeaders::add → KILLED
    existenceHeaders.add("Authorization", "Bearer " + token);
211 1 1. createTeamRepository : removed call to org/springframework/http/HttpHeaders::add → KILLED
    existenceHeaders.add("Accept", "application/vnd.github+json");
212 1 1. createTeamRepository : removed call to org/springframework/http/HttpHeaders::add → KILLED
    existenceHeaders.add("X-GitHub-Api-Version", "2022-11-28");
213
214
    HttpEntity<String> existenceEntity = new HttpEntity<>(existenceHeaders);
215
216
    try {
217
      restTemplate.exchange(existenceEndpoint, HttpMethod.GET, existenceEntity, String.class);
218
    } catch (HttpClientErrorException e) {
219 1 1. createTeamRepository : negated conditional → KILLED
      if (e.getStatusCode().equals(HttpStatus.NOT_FOUND)) {
220
        HttpHeaders createHeaders = new HttpHeaders();
221 1 1. createTeamRepository : removed call to org/springframework/http/HttpHeaders::add → KILLED
        createHeaders.add("Authorization", "Bearer " + token);
222 1 1. createTeamRepository : removed call to org/springframework/http/HttpHeaders::add → KILLED
        createHeaders.add("Accept", "application/vnd.github+json");
223 1 1. createTeamRepository : removed call to org/springframework/http/HttpHeaders::add → KILLED
        createHeaders.add("X-GitHub-Api-Version", "2022-11-28");
224
225
        Map<String, Object> body = new HashMap<>();
226
        body.put("name", newRepoName);
227
        body.put("private", isPrivate);
228
        String bodyAsJson = mapper.writeValueAsString(body);
229
230
        HttpEntity<String> createEntity = new HttpEntity<>(bodyAsJson, createHeaders);
231
232
        restTemplate.exchange(createEndpoint, HttpMethod.POST, createEntity, String.class);
233
      } else {
234
        log.warn(
235
            "Unexpected response code {} when checking for existence of repository {}",
236
            e.getStatusCode(),
237
            newRepoName);
238
        return;
239
      }
240
    }
241
    try {
242
      Map<String, Object> provisionBody = new HashMap<>();
243
      provisionBody.put("permission", permissions.getApiName());
244
      String provisionAsJson = mapper.writeValueAsString(provisionBody);
245
246
      HttpEntity<String> provisionEntity = new HttpEntity<>(provisionAsJson, existenceHeaders);
247
      restTemplate.exchange(provisionEndpoint, HttpMethod.PUT, provisionEntity, String.class);
248
    } catch (HttpClientErrorException ignored) {
249
      // silently ignore if provisioning fails (same as before)
250
    }
251
  }
252
}

Mutations

73

1.1
Location : createRepositoryForStudentOrStaff
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:exits_if_not_not_found()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

74

1.1
Location : createRepositoryForStudentOrStaff
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:exits_if_not_not_found()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

75

1.1
Location : createRepositoryForStudentOrStaff
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:exits_if_not_not_found()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

82

1.1
Location : createRepositoryForStudentOrStaff
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:exits_if_not_not_found()]
negated conditional → KILLED

84

1.1
Location : createRepositoryForStudentOrStaff
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:successfully_creates_staff_repo_public()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

85

1.1
Location : createRepositoryForStudentOrStaff
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:successfully_creates_staff_repo_public()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

86

1.1
Location : createRepositoryForStudentOrStaff
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:successfully_creates_staff_repo_public()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

141

1.1
Location : createStudentRepository
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:exits_if_not_not_found()]
removed call to edu/ucsb/cs156/frontiers/services/RepositoryService::createRepositoryForStudentOrStaff → KILLED

163

1.1
Location : createStaffRepository
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:successfully_creates_staff_repo_public()]
removed call to edu/ucsb/cs156/frontiers/services/RepositoryService::createRepositoryForStudentOrStaff → KILLED

210

1.1
Location : createTeamRepository
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:exits_if_team_repo_not_not_found()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

211

1.1
Location : createTeamRepository
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:exits_if_team_repo_not_not_found()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

212

1.1
Location : createTeamRepository
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:exits_if_team_repo_not_not_found()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

219

1.1
Location : createTeamRepository
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:exits_if_team_repo_not_not_found()]
negated conditional → KILLED

221

1.1
Location : createTeamRepository
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:successfully_creates_team_repo_private()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

222

1.1
Location : createTeamRepository
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:successfully_creates_team_repo_private()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

223

1.1
Location : createTeamRepository
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:successfully_creates_team_repo_private()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0